1
0
mirror of https://github.com/flutter/samples.git synced 2026-04-20 14:03:38 +00:00

Compass app (#2446)

This commit is contained in:
Eric Windmill
2024-09-27 18:49:27 -04:00
committed by GitHub
parent fcf2552cda
commit 46b5a26b26
326 changed files with 53272 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
class Assets {
static const activities = 'assets/activities.json';
static const destinations = 'assets/destinations.json';
}

View File

@@ -0,0 +1,144 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:provider/single_child_widget.dart';
import 'package:provider/provider.dart';
import '../data/repositories/auth/auth_repository.dart';
import '../data/repositories/auth/auth_repository_dev.dart';
import '../data/repositories/auth/auth_repository_remote.dart';
import '../data/repositories/booking/booking_repository.dart';
import '../data/repositories/booking/booking_repository_local.dart';
import '../data/repositories/booking/booking_repository_remote.dart';
import '../data/repositories/user/user_repository.dart';
import '../data/repositories/user/user_repository_local.dart';
import '../data/repositories/user/user_repository_remote.dart';
import '../data/services/api/auth_api_client.dart';
import '../data/services/local/local_data_service.dart';
import '../data/services/shared_preferences_service.dart';
import '../data/repositories/activity/activity_repository.dart';
import '../data/repositories/activity/activity_repository_local.dart';
import '../data/repositories/activity/activity_repository_remote.dart';
import '../data/repositories/continent/continent_repository.dart';
import '../data/repositories/continent/continent_repository_local.dart';
import '../data/repositories/continent/continent_repository_remote.dart';
import '../data/repositories/destination/destination_repository.dart';
import '../data/repositories/destination/destination_repository_local.dart';
import '../data/repositories/destination/destination_repository_remote.dart';
import '../data/repositories/itinerary_config/itinerary_config_repository.dart';
import '../data/repositories/itinerary_config/itinerary_config_repository_memory.dart';
import '../data/services/api/api_client.dart';
import '../domain/use_cases/booking/booking_create_use_case.dart';
import '../domain/use_cases/booking/booking_share_use_case.dart';
/// Shared providers for all configurations.
List<SingleChildWidget> _sharedProviders = [
Provider(
lazy: true,
create: (context) => BookingCreateUseCase(
destinationRepository: context.read(),
activityRepository: context.read(),
bookingRepository: context.read(),
),
),
Provider(
lazy: true,
create: (context) => BookingShareUseCase.withSharePlus(),
),
];
/// Configure dependencies for remote data.
/// This dependency list uses repositories that connect to a remote server.
List<SingleChildWidget> get providersRemote {
return [
Provider(
create: (context) => AuthApiClient(),
),
Provider(
create: (context) => ApiClient(),
),
Provider(
create: (context) => SharedPreferencesService(),
),
ChangeNotifierProvider(
create: (context) => AuthRepositoryRemote(
authApiClient: context.read(),
apiClient: context.read(),
sharedPreferencesService: context.read(),
) as AuthRepository,
),
Provider(
create: (context) => DestinationRepositoryRemote(
apiClient: context.read(),
) as DestinationRepository,
),
Provider(
create: (context) => ContinentRepositoryRemote(
apiClient: context.read(),
) as ContinentRepository,
),
Provider(
create: (context) => ActivityRepositoryRemote(
apiClient: context.read(),
) as ActivityRepository,
),
Provider.value(
value: ItineraryConfigRepositoryMemory() as ItineraryConfigRepository,
),
Provider(
create: (context) => BookingRepositoryRemote(
apiClient: context.read(),
) as BookingRepository,
),
Provider(
create: (context) => UserRepositoryRemote(
apiClient: context.read(),
) as UserRepository,
),
..._sharedProviders,
];
}
/// Configure dependencies for local data.
/// This dependency list uses repositories that provide local data.
/// The user is always logged in.
List<SingleChildWidget> get providersLocal {
return [
ChangeNotifierProvider.value(
value: AuthRepositoryDev() as AuthRepository,
),
Provider.value(
value: LocalDataService(),
),
Provider(
create: (context) => DestinationRepositoryLocal(
localDataService: context.read(),
) as DestinationRepository,
),
Provider(
create: (context) => ContinentRepositoryLocal(
localDataService: context.read(),
) as ContinentRepository,
),
Provider(
create: (context) => ActivityRepositoryLocal(
localDataService: context.read(),
) as ActivityRepository,
),
Provider(
create: (context) => BookingRepositoryLocal(
localDataService: context.read(),
) as BookingRepository,
),
Provider.value(
value: ItineraryConfigRepositoryMemory() as ItineraryConfigRepository,
),
Provider(
create: (context) => UserRepositoryLocal(
localDataService: context.read(),
) as UserRepository,
),
..._sharedProviders,
];
}

View File

@@ -0,0 +1,12 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/activity/activity.dart';
import '../../../utils/result.dart';
/// Data source for activities.
abstract class ActivityRepository {
/// Get activities by [Destination] ref.
Future<Result<List<Activity>>> getByDestination(String ref);
}

View File

@@ -0,0 +1,31 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/activity/activity.dart';
import '../../../utils/result.dart';
import '../../services/local/local_data_service.dart';
import 'activity_repository.dart';
/// Local implementation of ActivityRepository
/// Uses data from assets folder
class ActivityRepositoryLocal implements ActivityRepository {
ActivityRepositoryLocal({
required LocalDataService localDataService,
}) : _localDataService = localDataService;
final LocalDataService _localDataService;
@override
Future<Result<List<Activity>>> getByDestination(String ref) async {
try {
final activities = (await _localDataService.getActivities())
.where((activity) => activity.destinationRef == ref)
.toList();
return Result.ok(activities);
} on Exception catch (error) {
return Result.error(error);
}
}
}

View File

@@ -0,0 +1,36 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/activity/activity.dart';
import '../../../utils/result.dart';
import '../../services/api/api_client.dart';
import 'activity_repository.dart';
/// Remote data source for [Activity].
/// Implements basic local caching.
/// See: https://docs.flutter.dev/get-started/fwe/local-caching
class ActivityRepositoryRemote implements ActivityRepository {
ActivityRepositoryRemote({
required ApiClient apiClient,
}) : _apiClient = apiClient;
final ApiClient _apiClient;
final Map<String, List<Activity>> _cachedData = {};
@override
Future<Result<List<Activity>>> getByDestination(String ref) async {
if (!_cachedData.containsKey(ref)) {
// No cached data, request activities
final result = await _apiClient.getActivityByDestination(ref);
if (result is Ok) {
_cachedData[ref] = result.asOk.value;
}
return result;
} else {
// Return cached data if available
return Result.ok(_cachedData[ref]!);
}
}
}

View File

@@ -0,0 +1,22 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import '../../../utils/result.dart';
abstract class AuthRepository extends ChangeNotifier {
/// Returns true when the user is logged in
/// Returns [Future] because it will load a stored auth state the first time.
Future<bool> get isAuthenticated;
/// Perform login
Future<Result<void>> login({
required String email,
required String password,
});
/// Perform logout
Future<Result<void>> logout();
}

View File

@@ -0,0 +1,27 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../utils/result.dart';
import 'auth_repository.dart';
class AuthRepositoryDev extends AuthRepository {
/// User is always authenticated in dev scenarios
@override
Future<bool> get isAuthenticated => Future.value(true);
/// Login is always successful in dev scenarios
@override
Future<Result<void>> login({
required String email,
required String password,
}) async {
return Result.ok(null);
}
/// Logout is always successful in dev scenarios
@override
Future<Result<void>> logout() async {
return Result.ok(null);
}
}

View File

@@ -0,0 +1,112 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:logging/logging.dart';
import '../../../utils/result.dart';
import '../../services/api/api_client.dart';
import '../../services/api/auth_api_client.dart';
import '../../services/api/model/login_request/login_request.dart';
import '../../services/api/model/login_response/login_response.dart';
import '../../services/shared_preferences_service.dart';
import 'auth_repository.dart';
class AuthRepositoryRemote extends AuthRepository {
AuthRepositoryRemote({
required ApiClient apiClient,
required AuthApiClient authApiClient,
required SharedPreferencesService sharedPreferencesService,
}) : _apiClient = apiClient,
_authApiClient = authApiClient,
_sharedPreferencesService = sharedPreferencesService {
_apiClient.authHeaderProvider = _authHeaderProvider;
}
final AuthApiClient _authApiClient;
final ApiClient _apiClient;
final SharedPreferencesService _sharedPreferencesService;
bool? _isAuthenticated;
String? _authToken;
final _log = Logger('AuthRepositoryRemote');
/// Fetch token from shared preferences
Future<void> _fetch() async {
final result = await _sharedPreferencesService.fetchToken();
switch (result) {
case Ok<String?>():
_authToken = result.value;
_isAuthenticated = result.value != null;
case Error<String?>():
_log.severe(
'Failed to fech Token from SharedPreferences',
result.error,
);
}
}
@override
Future<bool> get isAuthenticated async {
// Status is cached
if (_isAuthenticated != null) {
return _isAuthenticated!;
}
// No status cached, fetch from storage
await _fetch();
return _isAuthenticated ?? false;
}
@override
Future<Result<void>> login({
required String email,
required String password,
}) async {
try {
final result = await _authApiClient.login(
LoginRequest(
email: email,
password: password,
),
);
switch (result) {
case Ok<LoginResponse>():
_log.info('User logged int');
// Set auth status
_isAuthenticated = true;
_authToken = result.value.token;
// Store in Shared preferences
return await _sharedPreferencesService.saveToken(result.value.token);
case Error<LoginResponse>():
_log.warning('Error logging in: ${result.error}');
return Result.error(result.error);
}
} finally {
notifyListeners();
}
}
@override
Future<Result<void>> logout() async {
_log.info('User logged out');
try {
// Clear stored auth token
final result = await _sharedPreferencesService.saveToken(null);
if (result is Error<void>) {
_log.severe('Failed to clear stored auth token');
}
// Clear token in ApiClient
_authToken = null;
// Clear authenticated status
_isAuthenticated = false;
return result;
} finally {
notifyListeners();
}
}
String? _authHeaderProvider() =>
_authToken != null ? 'Bearer $_authToken' : null;
}

View File

@@ -0,0 +1,21 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/booking/booking.dart';
import '../../../domain/models/booking/booking_summary.dart';
import '../../../utils/result.dart';
abstract class BookingRepository {
/// Returns the list of [BookingSummary] for the current user.
Future<Result<List<BookingSummary>>> getBookingsList();
/// Returns a full [Booking] given the id.
Future<Result<Booking>> getBooking(int id);
/// Creates a new [Booking].
Future<Result<void>> createBooking(Booking booking);
/// Delete booking
Future<Result<void>> delete(int id);
}

View File

@@ -0,0 +1,97 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:collection/collection.dart';
import '../../../domain/models/booking/booking.dart';
import '../../../domain/models/booking/booking_summary.dart';
import '../../../utils/result.dart';
import '../../services/local/local_data_service.dart';
import 'booking_repository.dart';
class BookingRepositoryLocal implements BookingRepository {
BookingRepositoryLocal({
required LocalDataService localDataService,
}) : _localDataService = localDataService;
// Only create default booking once
bool _isInitialized = false;
// Used to generate IDs for bookings
int _sequentialId = 0;
final _bookings = List<Booking>.empty(growable: true);
final LocalDataService _localDataService;
@override
Future<Result<void>> createBooking(Booking booking) async {
// Bookings created come without id, we need to assign one
final bookingWithId = booking.copyWith(id: _sequentialId++);
_bookings.add(bookingWithId);
return Result.ok(null);
}
@override
Future<Result<Booking>> getBooking(int id) async {
final booking = _bookings.firstWhereOrNull((booking) => booking.id == id);
if (booking == null) {
return Result.error(Exception('Booking not found'));
}
return Result.ok(booking);
}
@override
Future<Result<List<BookingSummary>>> getBookingsList() async {
// Initialize the repository with a default booking
if (!_isInitialized) {
await _createDefaultBooking();
_isInitialized = true;
}
return Result.ok(_createSummaries());
}
List<BookingSummary> _createSummaries() {
return _bookings
.map(
(booking) => BookingSummary(
id: booking.id!,
name:
'${booking.destination.name}, ${booking.destination.continent}',
startDate: booking.startDate,
endDate: booking.endDate,
),
)
.toList();
}
Future<void> _createDefaultBooking() async {
// create a default booking the first time
if (_bookings.isEmpty) {
final destination = (await _localDataService.getDestinations()).first;
final activities = (await _localDataService.getActivities())
.where((activity) => activity.destinationRef == destination.ref)
.take(4)
.toList();
_bookings.add(
Booking(
id: _sequentialId++,
startDate: DateTime(2024, 1, 1),
endDate: DateTime(2024, 2, 1),
destination: destination,
activity: activities,
),
);
}
}
@override
Future<Result<void>> delete(int id) async {
_bookings.removeWhere((booking) => booking.id == id);
return Result.ok(null);
}
}

View File

@@ -0,0 +1,118 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/activity/activity.dart';
import '../../../domain/models/booking/booking.dart';
import '../../../domain/models/booking/booking_summary.dart';
import '../../../domain/models/destination/destination.dart';
import '../../../utils/result.dart';
import '../../services/api/api_client.dart';
import '../../services/api/model/booking/booking_api_model.dart';
import 'booking_repository.dart';
class BookingRepositoryRemote implements BookingRepository {
BookingRepositoryRemote({
required ApiClient apiClient,
}) : _apiClient = apiClient;
final ApiClient _apiClient;
List<Destination>? _cachedDestinations;
@override
Future<Result<void>> createBooking(Booking booking) async {
try {
final BookingApiModel bookingApiModel = BookingApiModel(
startDate: booking.startDate,
endDate: booking.endDate,
name: '${booking.destination.name}, ${booking.destination.continent}',
destinationRef: booking.destination.ref,
activitiesRef:
booking.activity.map((activity) => activity.ref).toList(),
);
return _apiClient.postBooking(bookingApiModel);
} on Exception catch (e) {
return Result.error(e);
}
}
@override
Future<Result<Booking>> getBooking(int id) async {
try {
// Get booking by ID from server
final resultBooking = await _apiClient.getBooking(id);
if (resultBooking is Error<BookingApiModel>) {
return Result.error(resultBooking.error);
}
final booking = resultBooking.asOk.value;
// Load destinations if not loaded yet
if (_cachedDestinations == null) {
final resultDestination = await _apiClient.getDestinations();
if (resultDestination is Error<List<Destination>>) {
return Result.error(resultDestination.error);
}
_cachedDestinations = resultDestination.asOk.value;
}
// Get destination for booking
final destination = _cachedDestinations!.firstWhere(
(destination) => destination.ref == booking.destinationRef);
final resultActivities =
await _apiClient.getActivityByDestination(destination.ref);
if (resultActivities is Error<List<Activity>>) {
return Result.error(resultActivities.error);
}
final activities = resultActivities.asOk.value
.where((activity) => booking.activitiesRef.contains(activity.ref))
.toList();
return Result.ok(
Booking(
id: booking.id,
startDate: booking.startDate,
endDate: booking.endDate,
destination: destination,
activity: activities,
),
);
} on Exception catch (e) {
return Result.error(e);
}
}
@override
Future<Result<List<BookingSummary>>> getBookingsList() async {
try {
final result = await _apiClient.getBookings();
if (result is Error<List<BookingApiModel>>) {
return Result.error(result.error);
}
final bookingsApi = result.asOk.value;
return Result.ok(bookingsApi
.map(
(bookingApi) => BookingSummary(
id: bookingApi.id!,
name: bookingApi.name,
startDate: bookingApi.startDate,
endDate: bookingApi.endDate,
),
)
.toList());
} on Exception catch (e) {
return Result.error(e);
}
}
@override
Future<Result<void>> delete(int id) async {
try {
return _apiClient.deleteBooking(id);
} on Exception catch (e) {
return Result.error(e);
}
}
}

View File

@@ -0,0 +1,12 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/continent/continent.dart';
import '../../../utils/result.dart';
/// Data source with all possible continents.
abstract class ContinentRepository {
/// Get complete list of continents.
Future<Result<List<Continent>>> getContinents();
}

View File

@@ -0,0 +1,22 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/continent/continent.dart';
import '../../../utils/result.dart';
import '../../services/local/local_data_service.dart';
import 'continent_repository.dart';
/// Local data source with all possible continents.
class ContinentRepositoryLocal implements ContinentRepository {
ContinentRepositoryLocal({
required LocalDataService localDataService,
}) : _localDataService = localDataService;
final LocalDataService _localDataService;
@override
Future<Result<List<Continent>>> getContinents() async {
return Future.value(Result.ok(_localDataService.getContinents()));
}
}

View File

@@ -0,0 +1,37 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/continent/continent.dart';
import '../../../utils/result.dart';
import '../../services/api/api_client.dart';
import 'continent_repository.dart';
/// Remote data source for [Continent].
/// Implements basic local caching.
/// See: https://docs.flutter.dev/get-started/fwe/local-caching
class ContinentRepositoryRemote implements ContinentRepository {
ContinentRepositoryRemote({
required ApiClient apiClient,
}) : _apiClient = apiClient;
final ApiClient _apiClient;
List<Continent>? _cachedData;
@override
Future<Result<List<Continent>>> getContinents() async {
if (_cachedData == null) {
// No cached data, request continents
final result = await _apiClient.getContinents();
if (result is Ok) {
// Store value if result Ok
_cachedData = result.asOk.value;
}
return result;
} else {
// Return cached data if available
return Result.ok(_cachedData!);
}
}
}

View File

@@ -0,0 +1,12 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/destination/destination.dart';
import '../../../utils/result.dart';
/// Data source with all possible destinations
abstract class DestinationRepository {
/// Get complete list of destinations
Future<Result<List<Destination>>> getDestinations();
}

View File

@@ -0,0 +1,28 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/destination/destination.dart';
import '../../../utils/result.dart';
import '../../services/local/local_data_service.dart';
import 'destination_repository.dart';
/// Local implementation of DestinationRepository
/// Uses data from assets folder
class DestinationRepositoryLocal implements DestinationRepository {
DestinationRepositoryLocal({
required LocalDataService localDataService,
}) : _localDataService = localDataService;
final LocalDataService _localDataService;
/// Obtain list of destinations from local assets
@override
Future<Result<List<Destination>>> getDestinations() async {
try {
return Result.ok(await _localDataService.getDestinations());
} on Exception catch (error) {
return Result.error(error);
}
}
}

View File

@@ -0,0 +1,37 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/destination/destination.dart';
import '../../../utils/result.dart';
import '../../services/api/api_client.dart';
import 'destination_repository.dart';
/// Remote data source for [Destination].
/// Implements basic local caching.
/// See: https://docs.flutter.dev/get-started/fwe/local-caching
class DestinationRepositoryRemote implements DestinationRepository {
DestinationRepositoryRemote({
required ApiClient apiClient,
}) : _apiClient = apiClient;
final ApiClient _apiClient;
List<Destination>? _cachedData;
@override
Future<Result<List<Destination>>> getDestinations() async {
if (_cachedData == null) {
// No cached data, request destinations
final result = await _apiClient.getDestinations();
if (result is Ok) {
// Store value if result Ok
_cachedData = result.asOk.value;
}
return result;
} else {
// Return cached data if available
return Result.ok(_cachedData!);
}
}
}

View File

@@ -0,0 +1,17 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/itinerary_config/itinerary_config.dart';
import '../../../utils/result.dart';
/// Data source for the current [ItineraryConfig]
abstract class ItineraryConfigRepository {
/// Get current [ItineraryConfig], may be empty if no configuration started.
/// Method is async to support writing to database, file, etc.
Future<Result<ItineraryConfig>> getItineraryConfig();
/// Sets [ItineraryConfig], overrides the previous one stored.
/// Returns Result.Ok if set is successful.
Future<Result<void>> setItineraryConfig(ItineraryConfig itineraryConfig);
}

View File

@@ -0,0 +1,27 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import '../../../domain/models/itinerary_config/itinerary_config.dart';
import '../../../utils/result.dart';
import 'itinerary_config_repository.dart';
/// In-memory implementation of [ItineraryConfigRepository].
class ItineraryConfigRepositoryMemory implements ItineraryConfigRepository {
ItineraryConfig? _itineraryConfig;
@override
Future<Result<ItineraryConfig>> getItineraryConfig() async {
return Result.ok(_itineraryConfig ?? const ItineraryConfig());
}
@override
Future<Result<bool>> setItineraryConfig(
ItineraryConfig itineraryConfig,
) async {
_itineraryConfig = itineraryConfig;
return Result.ok(true);
}
}

View File

@@ -0,0 +1,12 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/user/user.dart';
import '../../../utils/result.dart';
/// Data source for user related data
abstract class UserRepository {
/// Get current user
Future<Result<User>> getUser();
}

View File

@@ -0,0 +1,21 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/user/user.dart';
import '../../../utils/result.dart';
import '../../services/local/local_data_service.dart';
import 'user_repository.dart';
class UserRepositoryLocal implements UserRepository {
UserRepositoryLocal({
required LocalDataService localDataService,
}) : _localDataService = localDataService;
final LocalDataService _localDataService;
@override
Future<Result<User>> getUser() async {
return Result.ok(_localDataService.getUser());
}
}

View File

@@ -0,0 +1,39 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../domain/models/user/user.dart';
import '../../../utils/result.dart';
import '../../services/api/api_client.dart';
import '../../services/api/model/user/user_api_model.dart';
import 'user_repository.dart';
class UserRepositoryRemote implements UserRepository {
UserRepositoryRemote({
required ApiClient apiClient,
}) : _apiClient = apiClient;
final ApiClient _apiClient;
User? _cachedData;
@override
Future<Result<User>> getUser() async {
if (_cachedData != null) {
return Future.value(Result.ok(_cachedData!));
}
final result = await _apiClient.getUser();
switch (result) {
case Ok<UserApiModel>():
final user = User(
name: result.value.name,
picture: result.value.picture,
);
_cachedData = user;
return Result.ok(user);
case Error<UserApiModel>():
return Result.error(result.error);
}
}
}

View File

@@ -0,0 +1,210 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:io';
import '../../../domain/models/activity/activity.dart';
import '../../../domain/models/continent/continent.dart';
import '../../../domain/models/destination/destination.dart';
import '../../../utils/result.dart';
import 'model/booking/booking_api_model.dart';
import 'model/user/user_api_model.dart';
/// Adds the `Authentication` header to a header configuration.
typedef AuthHeaderProvider = String? Function();
class ApiClient {
ApiClient({
String? host,
int? port,
HttpClient Function()? clientFactory,
}) : _host = host ?? 'localhost',
_port = port ?? 8080,
_clientFactory = clientFactory ?? (() => HttpClient());
final String _host;
final int _port;
final HttpClient Function() _clientFactory;
AuthHeaderProvider? _authHeaderProvider;
set authHeaderProvider(AuthHeaderProvider authHeaderProvider) {
_authHeaderProvider = authHeaderProvider;
}
Future<void> _authHeader(HttpHeaders headers) async {
final header = _authHeaderProvider?.call();
if (header != null) {
headers.add(HttpHeaders.authorizationHeader, header);
}
}
Future<Result<List<Continent>>> getContinents() async {
final client = _clientFactory();
try {
final request = await client.get(_host, _port, '/continent');
await _authHeader(request.headers);
final response = await request.close();
if (response.statusCode == 200) {
final stringData = await response.transform(utf8.decoder).join();
final json = jsonDecode(stringData) as List<dynamic>;
return Result.ok(
json.map((element) => Continent.fromJson(element)).toList());
} else {
return Result.error(const HttpException("Invalid response"));
}
} on Exception catch (error) {
return Result.error(error);
} finally {
client.close();
}
}
Future<Result<List<Destination>>> getDestinations() async {
final client = _clientFactory();
try {
final request = await client.get(_host, _port, '/destination');
await _authHeader(request.headers);
final response = await request.close();
if (response.statusCode == 200) {
final stringData = await response.transform(utf8.decoder).join();
final json = jsonDecode(stringData) as List<dynamic>;
return Result.ok(
json.map((element) => Destination.fromJson(element)).toList());
} else {
return Result.error(const HttpException("Invalid response"));
}
} on Exception catch (error) {
return Result.error(error);
} finally {
client.close();
}
}
Future<Result<List<Activity>>> getActivityByDestination(String ref) async {
final client = _clientFactory();
try {
final request =
await client.get(_host, _port, '/destination/$ref/activity');
await _authHeader(request.headers);
final response = await request.close();
if (response.statusCode == 200) {
final stringData = await response.transform(utf8.decoder).join();
final json = jsonDecode(stringData) as List<dynamic>;
final activities =
json.map((element) => Activity.fromJson(element)).toList();
return Result.ok(activities);
} else {
return Result.error(const HttpException("Invalid response"));
}
} on Exception catch (error) {
return Result.error(error);
} finally {
client.close();
}
}
Future<Result<List<BookingApiModel>>> getBookings() async {
final client = _clientFactory();
try {
final request = await client.get(_host, _port, '/booking');
await _authHeader(request.headers);
final response = await request.close();
if (response.statusCode == 200) {
final stringData = await response.transform(utf8.decoder).join();
final json = jsonDecode(stringData) as List<dynamic>;
final bookings =
json.map((element) => BookingApiModel.fromJson(element)).toList();
return Result.ok(bookings);
} else {
return Result.error(const HttpException("Invalid response"));
}
} on Exception catch (error) {
return Result.error(error);
} finally {
client.close();
}
}
Future<Result<BookingApiModel>> getBooking(int id) async {
final client = _clientFactory();
try {
final request = await client.get(_host, _port, '/booking/$id');
await _authHeader(request.headers);
final response = await request.close();
if (response.statusCode == 200) {
final stringData = await response.transform(utf8.decoder).join();
final booking = BookingApiModel.fromJson(jsonDecode(stringData));
return Result.ok(booking);
} else {
return Result.error(const HttpException("Invalid response"));
}
} on Exception catch (error) {
return Result.error(error);
} finally {
client.close();
}
}
Future<Result<BookingApiModel>> postBooking(BookingApiModel booking) async {
final client = _clientFactory();
try {
final request = await client.post(_host, _port, '/booking');
await _authHeader(request.headers);
request.write(jsonEncode(booking));
final response = await request.close();
if (response.statusCode == 201) {
final stringData = await response.transform(utf8.decoder).join();
final booking = BookingApiModel.fromJson(jsonDecode(stringData));
return Result.ok(booking);
} else {
return Result.error(const HttpException("Invalid response"));
}
} on Exception catch (error) {
return Result.error(error);
} finally {
client.close();
}
}
Future<Result<UserApiModel>> getUser() async {
final client = _clientFactory();
try {
final request = await client.get(_host, _port, '/user');
await _authHeader(request.headers);
final response = await request.close();
if (response.statusCode == 200) {
final stringData = await response.transform(utf8.decoder).join();
final user = UserApiModel.fromJson(jsonDecode(stringData));
return Result.ok(user);
} else {
return Result.error(const HttpException("Invalid response"));
}
} on Exception catch (error) {
return Result.error(error);
} finally {
client.close();
}
}
Future<Result<void>> deleteBooking(int id) async {
final client = _clientFactory();
try {
final request = await client.delete(_host, _port, '/booking/$id');
await _authHeader(request.headers);
final response = await request.close();
// Response 204 "No Content", delete was successful
if (response.statusCode == 204) {
return Result.ok(null);
} else {
return Result.error(const HttpException("Invalid response"));
}
} on Exception catch (error) {
return Result.error(error);
} finally {
client.close();
}
}
}

View File

@@ -0,0 +1,43 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:io';
import '../../../utils/result.dart';
import 'model/login_request/login_request.dart';
import 'model/login_response/login_response.dart';
class AuthApiClient {
AuthApiClient({
String? host,
int? port,
HttpClient Function()? clientFactory,
}) : _host = host ?? 'localhost',
_port = port ?? 8080,
_clientFactory = clientFactory ?? (() => HttpClient());
final String _host;
final int _port;
final HttpClient Function() _clientFactory;
Future<Result<LoginResponse>> login(LoginRequest loginRequest) async {
final client = _clientFactory();
try {
final request = await client.post(_host, _port, '/login');
request.write(jsonEncode(loginRequest));
final response = await request.close();
if (response.statusCode == 200) {
final stringData = await response.transform(utf8.decoder).join();
return Result.ok(LoginResponse.fromJson(jsonDecode(stringData)));
} else {
return Result.error(const HttpException("Login error"));
}
} on Exception catch (error) {
return Result.error(error);
} finally {
client.close();
}
}
}

View File

@@ -0,0 +1,35 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'booking_api_model.freezed.dart';
part 'booking_api_model.g.dart';
@freezed
class BookingApiModel with _$BookingApiModel {
const factory BookingApiModel({
/// Booking ID. Generated when stored in server.
int? id,
/// Start date of the trip
required DateTime startDate,
/// End date of the trip
required DateTime endDate,
/// Booking name
/// Should be "Destination, Continent"
required String name,
/// Destination of the trip
required String destinationRef,
/// List of chosen activities
required List<String> activitiesRef,
}) = _BookingApiModel;
factory BookingApiModel.fromJson(Map<String, Object?> json) =>
_$BookingApiModelFromJson(json);
}

View File

@@ -0,0 +1,317 @@
// 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_api_model.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');
BookingApiModel _$BookingApiModelFromJson(Map<String, dynamic> json) {
return _BookingApiModel.fromJson(json);
}
/// @nodoc
mixin _$BookingApiModel {
/// Booking ID. Generated when stored in server.
int? get id => throw _privateConstructorUsedError;
/// Start date of the trip
DateTime get startDate => throw _privateConstructorUsedError;
/// End date of the trip
DateTime get endDate => throw _privateConstructorUsedError;
/// Booking name
/// Should be "Destination, Continent"
String get name => throw _privateConstructorUsedError;
/// Destination of the trip
String get destinationRef => throw _privateConstructorUsedError;
/// List of chosen activities
List<String> get activitiesRef => throw _privateConstructorUsedError;
/// Serializes this BookingApiModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of BookingApiModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$BookingApiModelCopyWith<BookingApiModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $BookingApiModelCopyWith<$Res> {
factory $BookingApiModelCopyWith(
BookingApiModel value, $Res Function(BookingApiModel) then) =
_$BookingApiModelCopyWithImpl<$Res, BookingApiModel>;
@useResult
$Res call(
{int? id,
DateTime startDate,
DateTime endDate,
String name,
String destinationRef,
List<String> activitiesRef});
}
/// @nodoc
class _$BookingApiModelCopyWithImpl<$Res, $Val extends BookingApiModel>
implements $BookingApiModelCopyWith<$Res> {
_$BookingApiModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of BookingApiModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? startDate = null,
Object? endDate = null,
Object? name = null,
Object? destinationRef = null,
Object? activitiesRef = null,
}) {
return _then(_value.copyWith(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
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,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
destinationRef: null == destinationRef
? _value.destinationRef
: destinationRef // ignore: cast_nullable_to_non_nullable
as String,
activitiesRef: null == activitiesRef
? _value.activitiesRef
: activitiesRef // ignore: cast_nullable_to_non_nullable
as List<String>,
) as $Val);
}
}
/// @nodoc
abstract class _$$BookingApiModelImplCopyWith<$Res>
implements $BookingApiModelCopyWith<$Res> {
factory _$$BookingApiModelImplCopyWith(_$BookingApiModelImpl value,
$Res Function(_$BookingApiModelImpl) then) =
__$$BookingApiModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{int? id,
DateTime startDate,
DateTime endDate,
String name,
String destinationRef,
List<String> activitiesRef});
}
/// @nodoc
class __$$BookingApiModelImplCopyWithImpl<$Res>
extends _$BookingApiModelCopyWithImpl<$Res, _$BookingApiModelImpl>
implements _$$BookingApiModelImplCopyWith<$Res> {
__$$BookingApiModelImplCopyWithImpl(
_$BookingApiModelImpl _value, $Res Function(_$BookingApiModelImpl) _then)
: super(_value, _then);
/// Create a copy of BookingApiModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? startDate = null,
Object? endDate = null,
Object? name = null,
Object? destinationRef = null,
Object? activitiesRef = null,
}) {
return _then(_$BookingApiModelImpl(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
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,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
destinationRef: null == destinationRef
? _value.destinationRef
: destinationRef // ignore: cast_nullable_to_non_nullable
as String,
activitiesRef: null == activitiesRef
? _value._activitiesRef
: activitiesRef // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$BookingApiModelImpl implements _BookingApiModel {
const _$BookingApiModelImpl(
{this.id,
required this.startDate,
required this.endDate,
required this.name,
required this.destinationRef,
required final List<String> activitiesRef})
: _activitiesRef = activitiesRef;
factory _$BookingApiModelImpl.fromJson(Map<String, dynamic> json) =>
_$$BookingApiModelImplFromJson(json);
/// Booking ID. Generated when stored in server.
@override
final int? id;
/// Start date of the trip
@override
final DateTime startDate;
/// End date of the trip
@override
final DateTime endDate;
/// Booking name
/// Should be "Destination, Continent"
@override
final String name;
/// Destination of the trip
@override
final String destinationRef;
/// List of chosen activities
final List<String> _activitiesRef;
/// List of chosen activities
@override
List<String> get activitiesRef {
if (_activitiesRef is EqualUnmodifiableListView) return _activitiesRef;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_activitiesRef);
}
@override
String toString() {
return 'BookingApiModel(id: $id, startDate: $startDate, endDate: $endDate, name: $name, destinationRef: $destinationRef, activitiesRef: $activitiesRef)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$BookingApiModelImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.startDate, startDate) ||
other.startDate == startDate) &&
(identical(other.endDate, endDate) || other.endDate == endDate) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.destinationRef, destinationRef) ||
other.destinationRef == destinationRef) &&
const DeepCollectionEquality()
.equals(other._activitiesRef, _activitiesRef));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, startDate, endDate, name,
destinationRef, const DeepCollectionEquality().hash(_activitiesRef));
/// Create a copy of BookingApiModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$BookingApiModelImplCopyWith<_$BookingApiModelImpl> get copyWith =>
__$$BookingApiModelImplCopyWithImpl<_$BookingApiModelImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$BookingApiModelImplToJson(
this,
);
}
}
abstract class _BookingApiModel implements BookingApiModel {
const factory _BookingApiModel(
{final int? id,
required final DateTime startDate,
required final DateTime endDate,
required final String name,
required final String destinationRef,
required final List<String> activitiesRef}) = _$BookingApiModelImpl;
factory _BookingApiModel.fromJson(Map<String, dynamic> json) =
_$BookingApiModelImpl.fromJson;
/// Booking ID. Generated when stored in server.
@override
int? get id;
/// Start date of the trip
@override
DateTime get startDate;
/// End date of the trip
@override
DateTime get endDate;
/// Booking name
/// Should be "Destination, Continent"
@override
String get name;
/// Destination of the trip
@override
String get destinationRef;
/// List of chosen activities
@override
List<String> get activitiesRef;
/// Create a copy of BookingApiModel
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$BookingApiModelImplCopyWith<_$BookingApiModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,31 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'booking_api_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$BookingApiModelImpl _$$BookingApiModelImplFromJson(
Map<String, dynamic> json) =>
_$BookingApiModelImpl(
id: (json['id'] as num?)?.toInt(),
startDate: DateTime.parse(json['startDate'] as String),
endDate: DateTime.parse(json['endDate'] as String),
name: json['name'] as String,
destinationRef: json['destinationRef'] as String,
activitiesRef: (json['activitiesRef'] as List<dynamic>)
.map((e) => e as String)
.toList(),
);
Map<String, dynamic> _$$BookingApiModelImplToJson(
_$BookingApiModelImpl instance) =>
<String, dynamic>{
'id': instance.id,
'startDate': instance.startDate.toIso8601String(),
'endDate': instance.endDate.toIso8601String(),
'name': instance.name,
'destinationRef': instance.destinationRef,
'activitiesRef': instance.activitiesRef,
};

View File

@@ -0,0 +1,24 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'login_request.freezed.dart';
part 'login_request.g.dart';
/// Simple data class to hold login request data.
@freezed
class LoginRequest with _$LoginRequest {
const factory LoginRequest({
/// Email address.
required String email,
/// Plain text password.
required String password,
}) = _LoginRequest;
factory LoginRequest.fromJson(Map<String, Object?> json) =>
_$LoginRequestFromJson(json);
}

View File

@@ -0,0 +1,192 @@
// 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 'login_request.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');
LoginRequest _$LoginRequestFromJson(Map<String, dynamic> json) {
return _LoginRequest.fromJson(json);
}
/// @nodoc
mixin _$LoginRequest {
/// Email address.
String get email => throw _privateConstructorUsedError;
/// Plain text password.
String get password => throw _privateConstructorUsedError;
/// Serializes this LoginRequest to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of LoginRequest
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$LoginRequestCopyWith<LoginRequest> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LoginRequestCopyWith<$Res> {
factory $LoginRequestCopyWith(
LoginRequest value, $Res Function(LoginRequest) then) =
_$LoginRequestCopyWithImpl<$Res, LoginRequest>;
@useResult
$Res call({String email, String password});
}
/// @nodoc
class _$LoginRequestCopyWithImpl<$Res, $Val extends LoginRequest>
implements $LoginRequestCopyWith<$Res> {
_$LoginRequestCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LoginRequest
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? email = null,
Object? password = null,
}) {
return _then(_value.copyWith(
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
password: null == password
? _value.password
: password // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$LoginRequestImplCopyWith<$Res>
implements $LoginRequestCopyWith<$Res> {
factory _$$LoginRequestImplCopyWith(
_$LoginRequestImpl value, $Res Function(_$LoginRequestImpl) then) =
__$$LoginRequestImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String email, String password});
}
/// @nodoc
class __$$LoginRequestImplCopyWithImpl<$Res>
extends _$LoginRequestCopyWithImpl<$Res, _$LoginRequestImpl>
implements _$$LoginRequestImplCopyWith<$Res> {
__$$LoginRequestImplCopyWithImpl(
_$LoginRequestImpl _value, $Res Function(_$LoginRequestImpl) _then)
: super(_value, _then);
/// Create a copy of LoginRequest
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? email = null,
Object? password = null,
}) {
return _then(_$LoginRequestImpl(
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
password: null == password
? _value.password
: password // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$LoginRequestImpl implements _LoginRequest {
const _$LoginRequestImpl({required this.email, required this.password});
factory _$LoginRequestImpl.fromJson(Map<String, dynamic> json) =>
_$$LoginRequestImplFromJson(json);
/// Email address.
@override
final String email;
/// Plain text password.
@override
final String password;
@override
String toString() {
return 'LoginRequest(email: $email, password: $password)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LoginRequestImpl &&
(identical(other.email, email) || other.email == email) &&
(identical(other.password, password) ||
other.password == password));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, email, password);
/// Create a copy of LoginRequest
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$LoginRequestImplCopyWith<_$LoginRequestImpl> get copyWith =>
__$$LoginRequestImplCopyWithImpl<_$LoginRequestImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$LoginRequestImplToJson(
this,
);
}
}
abstract class _LoginRequest implements LoginRequest {
const factory _LoginRequest(
{required final String email,
required final String password}) = _$LoginRequestImpl;
factory _LoginRequest.fromJson(Map<String, dynamic> json) =
_$LoginRequestImpl.fromJson;
/// Email address.
@override
String get email;
/// Plain text password.
@override
String get password;
/// Create a copy of LoginRequest
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$LoginRequestImplCopyWith<_$LoginRequestImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'login_request.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$LoginRequestImpl _$$LoginRequestImplFromJson(Map<String, dynamic> json) =>
_$LoginRequestImpl(
email: json['email'] as String,
password: json['password'] as String,
);
Map<String, dynamic> _$$LoginRequestImplToJson(_$LoginRequestImpl instance) =>
<String, dynamic>{
'email': instance.email,
'password': instance.password,
};

View File

@@ -0,0 +1,24 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'login_response.freezed.dart';
part 'login_response.g.dart';
/// LoginResponse model.
@freezed
class LoginResponse with _$LoginResponse {
const factory LoginResponse({
/// The token to be used for authentication.
required String token,
/// The user id.
required String userId,
}) = _LoginResponse;
factory LoginResponse.fromJson(Map<String, Object?> json) =>
_$LoginResponseFromJson(json);
}

View File

@@ -0,0 +1,191 @@
// 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 'login_response.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');
LoginResponse _$LoginResponseFromJson(Map<String, dynamic> json) {
return _LoginResponse.fromJson(json);
}
/// @nodoc
mixin _$LoginResponse {
/// The token to be used for authentication.
String get token => throw _privateConstructorUsedError;
/// The user id.
String get userId => throw _privateConstructorUsedError;
/// Serializes this LoginResponse to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of LoginResponse
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$LoginResponseCopyWith<LoginResponse> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $LoginResponseCopyWith<$Res> {
factory $LoginResponseCopyWith(
LoginResponse value, $Res Function(LoginResponse) then) =
_$LoginResponseCopyWithImpl<$Res, LoginResponse>;
@useResult
$Res call({String token, String userId});
}
/// @nodoc
class _$LoginResponseCopyWithImpl<$Res, $Val extends LoginResponse>
implements $LoginResponseCopyWith<$Res> {
_$LoginResponseCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of LoginResponse
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? token = null,
Object? userId = null,
}) {
return _then(_value.copyWith(
token: null == token
? _value.token
: token // ignore: cast_nullable_to_non_nullable
as String,
userId: null == userId
? _value.userId
: userId // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$LoginResponseImplCopyWith<$Res>
implements $LoginResponseCopyWith<$Res> {
factory _$$LoginResponseImplCopyWith(
_$LoginResponseImpl value, $Res Function(_$LoginResponseImpl) then) =
__$$LoginResponseImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String token, String userId});
}
/// @nodoc
class __$$LoginResponseImplCopyWithImpl<$Res>
extends _$LoginResponseCopyWithImpl<$Res, _$LoginResponseImpl>
implements _$$LoginResponseImplCopyWith<$Res> {
__$$LoginResponseImplCopyWithImpl(
_$LoginResponseImpl _value, $Res Function(_$LoginResponseImpl) _then)
: super(_value, _then);
/// Create a copy of LoginResponse
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? token = null,
Object? userId = null,
}) {
return _then(_$LoginResponseImpl(
token: null == token
? _value.token
: token // ignore: cast_nullable_to_non_nullable
as String,
userId: null == userId
? _value.userId
: userId // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$LoginResponseImpl implements _LoginResponse {
const _$LoginResponseImpl({required this.token, required this.userId});
factory _$LoginResponseImpl.fromJson(Map<String, dynamic> json) =>
_$$LoginResponseImplFromJson(json);
/// The token to be used for authentication.
@override
final String token;
/// The user id.
@override
final String userId;
@override
String toString() {
return 'LoginResponse(token: $token, userId: $userId)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LoginResponseImpl &&
(identical(other.token, token) || other.token == token) &&
(identical(other.userId, userId) || other.userId == userId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, token, userId);
/// Create a copy of LoginResponse
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$LoginResponseImplCopyWith<_$LoginResponseImpl> get copyWith =>
__$$LoginResponseImplCopyWithImpl<_$LoginResponseImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$LoginResponseImplToJson(
this,
);
}
}
abstract class _LoginResponse implements LoginResponse {
const factory _LoginResponse(
{required final String token,
required final String userId}) = _$LoginResponseImpl;
factory _LoginResponse.fromJson(Map<String, dynamic> json) =
_$LoginResponseImpl.fromJson;
/// The token to be used for authentication.
@override
String get token;
/// The user id.
@override
String get userId;
/// Create a copy of LoginResponse
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$LoginResponseImplCopyWith<_$LoginResponseImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'login_response.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$LoginResponseImpl _$$LoginResponseImplFromJson(Map<String, dynamic> json) =>
_$LoginResponseImpl(
token: json['token'] as String,
userId: json['userId'] as String,
);
Map<String, dynamic> _$$LoginResponseImplToJson(_$LoginResponseImpl instance) =>
<String, dynamic>{
'token': instance.token,
'userId': instance.userId,
};

View File

@@ -0,0 +1,28 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user_api_model.freezed.dart';
part 'user_api_model.g.dart';
@freezed
abstract class UserApiModel with _$UserApiModel {
const factory UserApiModel({
/// The user's ID.
required String id,
/// The user's name.
required String name,
/// The user's email.
required String email,
/// The user's picture URL.
required String picture,
}) = _UserApiModel;
factory UserApiModel.fromJson(Map<String, Object?> json) =>
_$UserApiModelFromJson(json);
}

View File

@@ -0,0 +1,241 @@
// 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 'user_api_model.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');
UserApiModel _$UserApiModelFromJson(Map<String, dynamic> json) {
return _UserApiModel.fromJson(json);
}
/// @nodoc
mixin _$UserApiModel {
/// The user's ID.
String get id => throw _privateConstructorUsedError;
/// The user's name.
String get name => throw _privateConstructorUsedError;
/// The user's email.
String get email => throw _privateConstructorUsedError;
/// The user's picture URL.
String get picture => throw _privateConstructorUsedError;
/// Serializes this UserApiModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of UserApiModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$UserApiModelCopyWith<UserApiModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UserApiModelCopyWith<$Res> {
factory $UserApiModelCopyWith(
UserApiModel value, $Res Function(UserApiModel) then) =
_$UserApiModelCopyWithImpl<$Res, UserApiModel>;
@useResult
$Res call({String id, String name, String email, String picture});
}
/// @nodoc
class _$UserApiModelCopyWithImpl<$Res, $Val extends UserApiModel>
implements $UserApiModelCopyWith<$Res> {
_$UserApiModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of UserApiModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? email = null,
Object? picture = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
picture: null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$UserApiModelImplCopyWith<$Res>
implements $UserApiModelCopyWith<$Res> {
factory _$$UserApiModelImplCopyWith(
_$UserApiModelImpl value, $Res Function(_$UserApiModelImpl) then) =
__$$UserApiModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String id, String name, String email, String picture});
}
/// @nodoc
class __$$UserApiModelImplCopyWithImpl<$Res>
extends _$UserApiModelCopyWithImpl<$Res, _$UserApiModelImpl>
implements _$$UserApiModelImplCopyWith<$Res> {
__$$UserApiModelImplCopyWithImpl(
_$UserApiModelImpl _value, $Res Function(_$UserApiModelImpl) _then)
: super(_value, _then);
/// Create a copy of UserApiModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? email = null,
Object? picture = null,
}) {
return _then(_$UserApiModelImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
email: null == email
? _value.email
: email // ignore: cast_nullable_to_non_nullable
as String,
picture: null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$UserApiModelImpl implements _UserApiModel {
const _$UserApiModelImpl(
{required this.id,
required this.name,
required this.email,
required this.picture});
factory _$UserApiModelImpl.fromJson(Map<String, dynamic> json) =>
_$$UserApiModelImplFromJson(json);
/// The user's ID.
@override
final String id;
/// The user's name.
@override
final String name;
/// The user's email.
@override
final String email;
/// The user's picture URL.
@override
final String picture;
@override
String toString() {
return 'UserApiModel(id: $id, name: $name, email: $email, picture: $picture)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UserApiModelImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.email, email) || other.email == email) &&
(identical(other.picture, picture) || other.picture == picture));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, name, email, picture);
/// Create a copy of UserApiModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$UserApiModelImplCopyWith<_$UserApiModelImpl> get copyWith =>
__$$UserApiModelImplCopyWithImpl<_$UserApiModelImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$UserApiModelImplToJson(
this,
);
}
}
abstract class _UserApiModel implements UserApiModel {
const factory _UserApiModel(
{required final String id,
required final String name,
required final String email,
required final String picture}) = _$UserApiModelImpl;
factory _UserApiModel.fromJson(Map<String, dynamic> json) =
_$UserApiModelImpl.fromJson;
/// The user's ID.
@override
String get id;
/// The user's name.
@override
String get name;
/// The user's email.
@override
String get email;
/// The user's picture URL.
@override
String get picture;
/// Create a copy of UserApiModel
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$UserApiModelImplCopyWith<_$UserApiModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,23 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_api_model.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$UserApiModelImpl _$$UserApiModelImplFromJson(Map<String, dynamic> json) =>
_$UserApiModelImpl(
id: json['id'] as String,
name: json['name'] as String,
email: json['email'] as String,
picture: json['picture'] as String,
);
Map<String, dynamic> _$$UserApiModelImplToJson(_$UserApiModelImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'email': instance.email,
'picture': instance.picture,
};

View File

@@ -0,0 +1,71 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'package:flutter/services.dart';
import '../../../config/assets.dart';
import '../../../domain/models/activity/activity.dart';
import '../../../domain/models/continent/continent.dart';
import '../../../domain/models/destination/destination.dart';
import '../../../domain/models/user/user.dart';
class LocalDataService {
List<Continent> getContinents() {
return [
const Continent(
name: 'Europe',
imageUrl: 'https://rstr.in/google/tripedia/TmR12QdlVTT',
),
const Continent(
name: 'Asia',
imageUrl: 'https://rstr.in/google/tripedia/VJ8BXlQg8O1',
),
const Continent(
name: 'South America',
imageUrl: 'https://rstr.in/google/tripedia/flm_-o1aI8e',
),
const Continent(
name: 'Africa',
imageUrl: 'https://rstr.in/google/tripedia/-nzi8yFOBpF',
),
const Continent(
name: 'North America',
imageUrl: 'https://rstr.in/google/tripedia/jlbgFDrSUVE',
),
const Continent(
name: 'Oceania',
imageUrl: 'https://rstr.in/google/tripedia/vxyrDE-fZVL',
),
const Continent(
name: 'Australia',
imageUrl: 'https://rstr.in/google/tripedia/z6vy6HeRyvZ',
),
];
}
Future<List<Activity>> getActivities() async {
final json = await _loadStringAsset(Assets.activities);
return json.map<Activity>((json) => Activity.fromJson(json)).toList();
}
Future<List<Destination>> getDestinations() async {
final json = await _loadStringAsset(Assets.destinations);
return json.map<Destination>((json) => Destination.fromJson(json)).toList();
}
Future<List<Map<String, dynamic>>> _loadStringAsset(String asset) async {
final localData = await rootBundle.loadString(asset);
return (jsonDecode(localData) as List).cast<Map<String, dynamic>>();
}
User getUser() {
return const User(
name: 'Sofie',
// For demo purposes we use a local asset
picture: 'assets/user.jpg',
);
}
}

View File

@@ -0,0 +1,41 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../utils/result.dart';
class SharedPreferencesService {
static const _tokenKey = 'TOKEN';
final _log = Logger('SharedPreferencesService');
Future<Result<String?>> fetchToken() async {
try {
final sharedPreferences = await SharedPreferences.getInstance();
_log.finer('Got token from SharedPreferences');
return Result.ok(sharedPreferences.getString(_tokenKey));
} on Exception catch (e) {
_log.warning('Failed to get token', e);
return Result.error(e);
}
}
Future<Result<void>> saveToken(String? token) async {
try {
final sharedPreferences = await SharedPreferences.getInstance();
if (token == null) {
_log.finer('Removed token');
await sharedPreferences.remove(_tokenKey);
} else {
_log.finer('Replaced token');
await sharedPreferences.setString(_tokenKey, token);
}
return Result.ok(null);
} on Exception catch (e) {
_log.warning('Failed to set token', e);
return Result.error(e);
}
}
}

View File

@@ -0,0 +1,56 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'activity.freezed.dart';
part 'activity.g.dart';
enum TimeOfDay {
any,
morning,
afternoon,
evening,
night,
}
@freezed
class Activity with _$Activity {
const factory Activity({
/// e.g. 'Glacier Trekking and Ice Climbing'
required String name,
/// e.g. 'Embark on a thrilling adventure exploring the awe-inspiring glaciers of Alaska. Hike across the icy terrain, marvel at the deep blue crevasses, and even try your hand at ice climbing for an unforgettable experience.'
required String description,
/// e.g. 'Matanuska Glacier or Mendenhall Glacier'
required String locationName,
/// Duration in days.
/// e.g. 8
required int duration,
/// e.g. 'morning'
required TimeOfDay timeOfDay,
/// e.g. false
required bool familyFriendly,
/// e.g. 4
required int price,
/// e.g. 'alaska'
required String destinationRef,
/// e.g. 'glacier-trekking-and-ice-climbing'
required String ref,
/// e.g. 'https://storage.googleapis.com/tripedia-images/activities/alaska_glacier-trekking-and-ice-climbing.jpg'
required String imageUrl,
}) = _Activity;
factory Activity.fromJson(Map<String, Object?> json) =>
_$ActivityFromJson(json);
}

View File

@@ -0,0 +1,425 @@
// 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 'activity.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');
Activity _$ActivityFromJson(Map<String, dynamic> json) {
return _Activity.fromJson(json);
}
/// @nodoc
mixin _$Activity {
/// e.g. 'Glacier Trekking and Ice Climbing'
String get name => throw _privateConstructorUsedError;
/// e.g. 'Embark on a thrilling adventure exploring the awe-inspiring glaciers of Alaska. Hike across the icy terrain, marvel at the deep blue crevasses, and even try your hand at ice climbing for an unforgettable experience.'
String get description => throw _privateConstructorUsedError;
/// e.g. 'Matanuska Glacier or Mendenhall Glacier'
String get locationName => throw _privateConstructorUsedError;
/// Duration in days.
/// e.g. 8
int get duration => throw _privateConstructorUsedError;
/// e.g. 'morning'
TimeOfDay get timeOfDay => throw _privateConstructorUsedError;
/// e.g. false
bool get familyFriendly => throw _privateConstructorUsedError;
/// e.g. 4
int get price => throw _privateConstructorUsedError;
/// e.g. 'alaska'
String get destinationRef => throw _privateConstructorUsedError;
/// e.g. 'glacier-trekking-and-ice-climbing'
String get ref => throw _privateConstructorUsedError;
/// e.g. 'https://storage.googleapis.com/tripedia-images/activities/alaska_glacier-trekking-and-ice-climbing.jpg'
String get imageUrl => throw _privateConstructorUsedError;
/// Serializes this Activity to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of Activity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ActivityCopyWith<Activity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ActivityCopyWith<$Res> {
factory $ActivityCopyWith(Activity value, $Res Function(Activity) then) =
_$ActivityCopyWithImpl<$Res, Activity>;
@useResult
$Res call(
{String name,
String description,
String locationName,
int duration,
TimeOfDay timeOfDay,
bool familyFriendly,
int price,
String destinationRef,
String ref,
String imageUrl});
}
/// @nodoc
class _$ActivityCopyWithImpl<$Res, $Val extends Activity>
implements $ActivityCopyWith<$Res> {
_$ActivityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Activity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? description = null,
Object? locationName = null,
Object? duration = null,
Object? timeOfDay = null,
Object? familyFriendly = null,
Object? price = null,
Object? destinationRef = null,
Object? ref = null,
Object? imageUrl = null,
}) {
return _then(_value.copyWith(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
locationName: null == locationName
? _value.locationName
: locationName // ignore: cast_nullable_to_non_nullable
as String,
duration: null == duration
? _value.duration
: duration // ignore: cast_nullable_to_non_nullable
as int,
timeOfDay: null == timeOfDay
? _value.timeOfDay
: timeOfDay // ignore: cast_nullable_to_non_nullable
as TimeOfDay,
familyFriendly: null == familyFriendly
? _value.familyFriendly
: familyFriendly // ignore: cast_nullable_to_non_nullable
as bool,
price: null == price
? _value.price
: price // ignore: cast_nullable_to_non_nullable
as int,
destinationRef: null == destinationRef
? _value.destinationRef
: destinationRef // ignore: cast_nullable_to_non_nullable
as String,
ref: null == ref
? _value.ref
: ref // ignore: cast_nullable_to_non_nullable
as String,
imageUrl: null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$ActivityImplCopyWith<$Res>
implements $ActivityCopyWith<$Res> {
factory _$$ActivityImplCopyWith(
_$ActivityImpl value, $Res Function(_$ActivityImpl) then) =
__$$ActivityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String name,
String description,
String locationName,
int duration,
TimeOfDay timeOfDay,
bool familyFriendly,
int price,
String destinationRef,
String ref,
String imageUrl});
}
/// @nodoc
class __$$ActivityImplCopyWithImpl<$Res>
extends _$ActivityCopyWithImpl<$Res, _$ActivityImpl>
implements _$$ActivityImplCopyWith<$Res> {
__$$ActivityImplCopyWithImpl(
_$ActivityImpl _value, $Res Function(_$ActivityImpl) _then)
: super(_value, _then);
/// Create a copy of Activity
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? description = null,
Object? locationName = null,
Object? duration = null,
Object? timeOfDay = null,
Object? familyFriendly = null,
Object? price = null,
Object? destinationRef = null,
Object? ref = null,
Object? imageUrl = null,
}) {
return _then(_$ActivityImpl(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
description: null == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String,
locationName: null == locationName
? _value.locationName
: locationName // ignore: cast_nullable_to_non_nullable
as String,
duration: null == duration
? _value.duration
: duration // ignore: cast_nullable_to_non_nullable
as int,
timeOfDay: null == timeOfDay
? _value.timeOfDay
: timeOfDay // ignore: cast_nullable_to_non_nullable
as TimeOfDay,
familyFriendly: null == familyFriendly
? _value.familyFriendly
: familyFriendly // ignore: cast_nullable_to_non_nullable
as bool,
price: null == price
? _value.price
: price // ignore: cast_nullable_to_non_nullable
as int,
destinationRef: null == destinationRef
? _value.destinationRef
: destinationRef // ignore: cast_nullable_to_non_nullable
as String,
ref: null == ref
? _value.ref
: ref // ignore: cast_nullable_to_non_nullable
as String,
imageUrl: null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ActivityImpl implements _Activity {
const _$ActivityImpl(
{required this.name,
required this.description,
required this.locationName,
required this.duration,
required this.timeOfDay,
required this.familyFriendly,
required this.price,
required this.destinationRef,
required this.ref,
required this.imageUrl});
factory _$ActivityImpl.fromJson(Map<String, dynamic> json) =>
_$$ActivityImplFromJson(json);
/// e.g. 'Glacier Trekking and Ice Climbing'
@override
final String name;
/// e.g. 'Embark on a thrilling adventure exploring the awe-inspiring glaciers of Alaska. Hike across the icy terrain, marvel at the deep blue crevasses, and even try your hand at ice climbing for an unforgettable experience.'
@override
final String description;
/// e.g. 'Matanuska Glacier or Mendenhall Glacier'
@override
final String locationName;
/// Duration in days.
/// e.g. 8
@override
final int duration;
/// e.g. 'morning'
@override
final TimeOfDay timeOfDay;
/// e.g. false
@override
final bool familyFriendly;
/// e.g. 4
@override
final int price;
/// e.g. 'alaska'
@override
final String destinationRef;
/// e.g. 'glacier-trekking-and-ice-climbing'
@override
final String ref;
/// e.g. 'https://storage.googleapis.com/tripedia-images/activities/alaska_glacier-trekking-and-ice-climbing.jpg'
@override
final String imageUrl;
@override
String toString() {
return 'Activity(name: $name, description: $description, locationName: $locationName, duration: $duration, timeOfDay: $timeOfDay, familyFriendly: $familyFriendly, price: $price, destinationRef: $destinationRef, ref: $ref, imageUrl: $imageUrl)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ActivityImpl &&
(identical(other.name, name) || other.name == name) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.locationName, locationName) ||
other.locationName == locationName) &&
(identical(other.duration, duration) ||
other.duration == duration) &&
(identical(other.timeOfDay, timeOfDay) ||
other.timeOfDay == timeOfDay) &&
(identical(other.familyFriendly, familyFriendly) ||
other.familyFriendly == familyFriendly) &&
(identical(other.price, price) || other.price == price) &&
(identical(other.destinationRef, destinationRef) ||
other.destinationRef == destinationRef) &&
(identical(other.ref, ref) || other.ref == ref) &&
(identical(other.imageUrl, imageUrl) ||
other.imageUrl == imageUrl));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
name,
description,
locationName,
duration,
timeOfDay,
familyFriendly,
price,
destinationRef,
ref,
imageUrl);
/// Create a copy of Activity
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ActivityImplCopyWith<_$ActivityImpl> get copyWith =>
__$$ActivityImplCopyWithImpl<_$ActivityImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ActivityImplToJson(
this,
);
}
}
abstract class _Activity implements Activity {
const factory _Activity(
{required final String name,
required final String description,
required final String locationName,
required final int duration,
required final TimeOfDay timeOfDay,
required final bool familyFriendly,
required final int price,
required final String destinationRef,
required final String ref,
required final String imageUrl}) = _$ActivityImpl;
factory _Activity.fromJson(Map<String, dynamic> json) =
_$ActivityImpl.fromJson;
/// e.g. 'Glacier Trekking and Ice Climbing'
@override
String get name;
/// e.g. 'Embark on a thrilling adventure exploring the awe-inspiring glaciers of Alaska. Hike across the icy terrain, marvel at the deep blue crevasses, and even try your hand at ice climbing for an unforgettable experience.'
@override
String get description;
/// e.g. 'Matanuska Glacier or Mendenhall Glacier'
@override
String get locationName;
/// Duration in days.
/// e.g. 8
@override
int get duration;
/// e.g. 'morning'
@override
TimeOfDay get timeOfDay;
/// e.g. false
@override
bool get familyFriendly;
/// e.g. 4
@override
int get price;
/// e.g. 'alaska'
@override
String get destinationRef;
/// e.g. 'glacier-trekking-and-ice-climbing'
@override
String get ref;
/// e.g. 'https://storage.googleapis.com/tripedia-images/activities/alaska_glacier-trekking-and-ice-climbing.jpg'
@override
String get imageUrl;
/// Create a copy of Activity
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ActivityImplCopyWith<_$ActivityImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,43 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'activity.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$ActivityImpl _$$ActivityImplFromJson(Map<String, dynamic> json) =>
_$ActivityImpl(
name: json['name'] as String,
description: json['description'] as String,
locationName: json['locationName'] as String,
duration: (json['duration'] as num).toInt(),
timeOfDay: $enumDecode(_$TimeOfDayEnumMap, json['timeOfDay']),
familyFriendly: json['familyFriendly'] as bool,
price: (json['price'] as num).toInt(),
destinationRef: json['destinationRef'] as String,
ref: json['ref'] as String,
imageUrl: json['imageUrl'] as String,
);
Map<String, dynamic> _$$ActivityImplToJson(_$ActivityImpl instance) =>
<String, dynamic>{
'name': instance.name,
'description': instance.description,
'locationName': instance.locationName,
'duration': instance.duration,
'timeOfDay': _$TimeOfDayEnumMap[instance.timeOfDay]!,
'familyFriendly': instance.familyFriendly,
'price': instance.price,
'destinationRef': instance.destinationRef,
'ref': instance.ref,
'imageUrl': instance.imageUrl,
};
const _$TimeOfDayEnumMap = {
TimeOfDay.any: 'any',
TimeOfDay.morning: 'morning',
TimeOfDay.afternoon: 'afternoon',
TimeOfDay.evening: 'evening',
TimeOfDay.night: 'night',
};

View File

@@ -0,0 +1,35 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:freezed_annotation/freezed_annotation.dart';
import '../activity/activity.dart';
import '../destination/destination.dart';
part 'booking.freezed.dart';
part 'booking.g.dart';
@freezed
class Booking with _$Booking {
const factory Booking({
/// Optional ID of the booking.
/// May be null if the booking is not yet stored.
int? id,
/// 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);
}

View File

@@ -0,0 +1,300 @@
// 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 {
/// Optional ID of the booking.
/// May be null if the booking is not yet stored.
int? get id => throw _privateConstructorUsedError;
/// 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(
{int? id,
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? id = freezed,
Object? startDate = null,
Object? endDate = null,
Object? destination = null,
Object? activity = null,
}) {
return _then(_value.copyWith(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
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(
{int? id,
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? id = freezed,
Object? startDate = null,
Object? endDate = null,
Object? destination = null,
Object? activity = null,
}) {
return _then(_$BookingImpl(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int?,
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(
{this.id,
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);
/// Optional ID of the booking.
/// May be null if the booking is not yet stored.
@override
final int? id;
/// 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(id: $id, 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.id, id) || other.id == id) &&
(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, id, 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(
{final int? id,
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;
/// Optional ID of the booking.
/// May be null if the booking is not yet stored.
@override
int? get id;
/// 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;
}

View File

@@ -0,0 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'booking.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$BookingImpl _$$BookingImplFromJson(Map<String, dynamic> json) =>
_$BookingImpl(
id: (json['id'] as num?)?.toInt(),
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>{
'id': instance.id,
'startDate': instance.startDate.toIso8601String(),
'endDate': instance.endDate.toIso8601String(),
'destination': instance.destination,
'activity': instance.activity,
};

View File

@@ -0,0 +1,34 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'booking_summary.freezed.dart';
part 'booking_summary.g.dart';
/// BookingSummary contains the necessary data to display a booking
/// in the user home screen, but lacks the rest of the booking data
/// like activitities or destination.
///
/// Use the [BookingRepository] to obtain a full [Booking]
/// using the [BookingSummary.id].
@freezed
class BookingSummary with _$BookingSummary {
const factory BookingSummary({
/// Booking id
required int id,
/// Name to be displayed
required String name,
/// Start date of the booking
required DateTime startDate,
/// End date of the booking
required DateTime endDate,
}) = _BookingSummary;
factory BookingSummary.fromJson(Map<String, Object?> json) =>
_$BookingSummaryFromJson(json);
}

View File

@@ -0,0 +1,243 @@
// 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_summary.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');
BookingSummary _$BookingSummaryFromJson(Map<String, dynamic> json) {
return _BookingSummary.fromJson(json);
}
/// @nodoc
mixin _$BookingSummary {
/// Booking id
int get id => throw _privateConstructorUsedError;
/// Name to be displayed
String get name => throw _privateConstructorUsedError;
/// Start date of the booking
DateTime get startDate => throw _privateConstructorUsedError;
/// End date of the booking
DateTime get endDate => throw _privateConstructorUsedError;
/// Serializes this BookingSummary to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of BookingSummary
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$BookingSummaryCopyWith<BookingSummary> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $BookingSummaryCopyWith<$Res> {
factory $BookingSummaryCopyWith(
BookingSummary value, $Res Function(BookingSummary) then) =
_$BookingSummaryCopyWithImpl<$Res, BookingSummary>;
@useResult
$Res call({int id, String name, DateTime startDate, DateTime endDate});
}
/// @nodoc
class _$BookingSummaryCopyWithImpl<$Res, $Val extends BookingSummary>
implements $BookingSummaryCopyWith<$Res> {
_$BookingSummaryCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of BookingSummary
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? startDate = null,
Object? endDate = null,
}) {
return _then(_value.copyWith(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
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,
) as $Val);
}
}
/// @nodoc
abstract class _$$BookingSummaryImplCopyWith<$Res>
implements $BookingSummaryCopyWith<$Res> {
factory _$$BookingSummaryImplCopyWith(_$BookingSummaryImpl value,
$Res Function(_$BookingSummaryImpl) then) =
__$$BookingSummaryImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({int id, String name, DateTime startDate, DateTime endDate});
}
/// @nodoc
class __$$BookingSummaryImplCopyWithImpl<$Res>
extends _$BookingSummaryCopyWithImpl<$Res, _$BookingSummaryImpl>
implements _$$BookingSummaryImplCopyWith<$Res> {
__$$BookingSummaryImplCopyWithImpl(
_$BookingSummaryImpl _value, $Res Function(_$BookingSummaryImpl) _then)
: super(_value, _then);
/// Create a copy of BookingSummary
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = null,
Object? name = null,
Object? startDate = null,
Object? endDate = null,
}) {
return _then(_$BookingSummaryImpl(
id: null == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as int,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
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,
));
}
}
/// @nodoc
@JsonSerializable()
class _$BookingSummaryImpl implements _BookingSummary {
const _$BookingSummaryImpl(
{required this.id,
required this.name,
required this.startDate,
required this.endDate});
factory _$BookingSummaryImpl.fromJson(Map<String, dynamic> json) =>
_$$BookingSummaryImplFromJson(json);
/// Booking id
@override
final int id;
/// Name to be displayed
@override
final String name;
/// Start date of the booking
@override
final DateTime startDate;
/// End date of the booking
@override
final DateTime endDate;
@override
String toString() {
return 'BookingSummary(id: $id, name: $name, startDate: $startDate, endDate: $endDate)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$BookingSummaryImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.startDate, startDate) ||
other.startDate == startDate) &&
(identical(other.endDate, endDate) || other.endDate == endDate));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, id, name, startDate, endDate);
/// Create a copy of BookingSummary
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$BookingSummaryImplCopyWith<_$BookingSummaryImpl> get copyWith =>
__$$BookingSummaryImplCopyWithImpl<_$BookingSummaryImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$BookingSummaryImplToJson(
this,
);
}
}
abstract class _BookingSummary implements BookingSummary {
const factory _BookingSummary(
{required final int id,
required final String name,
required final DateTime startDate,
required final DateTime endDate}) = _$BookingSummaryImpl;
factory _BookingSummary.fromJson(Map<String, dynamic> json) =
_$BookingSummaryImpl.fromJson;
/// Booking id
@override
int get id;
/// Name to be displayed
@override
String get name;
/// Start date of the booking
@override
DateTime get startDate;
/// End date of the booking
@override
DateTime get endDate;
/// Create a copy of BookingSummary
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$BookingSummaryImplCopyWith<_$BookingSummaryImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'booking_summary.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$BookingSummaryImpl _$$BookingSummaryImplFromJson(Map<String, dynamic> json) =>
_$BookingSummaryImpl(
id: (json['id'] as num).toInt(),
name: json['name'] as String,
startDate: DateTime.parse(json['startDate'] as String),
endDate: DateTime.parse(json['endDate'] as String),
);
Map<String, dynamic> _$$BookingSummaryImplToJson(
_$BookingSummaryImpl instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'startDate': instance.startDate.toIso8601String(),
'endDate': instance.endDate.toIso8601String(),
};

View File

@@ -0,0 +1,23 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'continent.freezed.dart';
part 'continent.g.dart';
@freezed
class Continent with _$Continent {
const factory Continent({
/// e.g. 'Europe'
required String name,
/// e.g. 'https://rstr.in/google/tripedia/TmR12QdlVTT'
required String imageUrl,
}) = _Continent;
factory Continent.fromJson(Map<String, Object?> json) =>
_$ContinentFromJson(json);
}

View File

@@ -0,0 +1,191 @@
// 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 'continent.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');
Continent _$ContinentFromJson(Map<String, dynamic> json) {
return _Continent.fromJson(json);
}
/// @nodoc
mixin _$Continent {
/// e.g. 'Europe'
String get name => throw _privateConstructorUsedError;
/// e.g. 'https://rstr.in/google/tripedia/TmR12QdlVTT'
String get imageUrl => throw _privateConstructorUsedError;
/// Serializes this Continent to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of Continent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ContinentCopyWith<Continent> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ContinentCopyWith<$Res> {
factory $ContinentCopyWith(Continent value, $Res Function(Continent) then) =
_$ContinentCopyWithImpl<$Res, Continent>;
@useResult
$Res call({String name, String imageUrl});
}
/// @nodoc
class _$ContinentCopyWithImpl<$Res, $Val extends Continent>
implements $ContinentCopyWith<$Res> {
_$ContinentCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Continent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? imageUrl = null,
}) {
return _then(_value.copyWith(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
imageUrl: null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$ContinentImplCopyWith<$Res>
implements $ContinentCopyWith<$Res> {
factory _$$ContinentImplCopyWith(
_$ContinentImpl value, $Res Function(_$ContinentImpl) then) =
__$$ContinentImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String name, String imageUrl});
}
/// @nodoc
class __$$ContinentImplCopyWithImpl<$Res>
extends _$ContinentCopyWithImpl<$Res, _$ContinentImpl>
implements _$$ContinentImplCopyWith<$Res> {
__$$ContinentImplCopyWithImpl(
_$ContinentImpl _value, $Res Function(_$ContinentImpl) _then)
: super(_value, _then);
/// Create a copy of Continent
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? imageUrl = null,
}) {
return _then(_$ContinentImpl(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
imageUrl: null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ContinentImpl implements _Continent {
const _$ContinentImpl({required this.name, required this.imageUrl});
factory _$ContinentImpl.fromJson(Map<String, dynamic> json) =>
_$$ContinentImplFromJson(json);
/// e.g. 'Europe'
@override
final String name;
/// e.g. 'https://rstr.in/google/tripedia/TmR12QdlVTT'
@override
final String imageUrl;
@override
String toString() {
return 'Continent(name: $name, imageUrl: $imageUrl)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ContinentImpl &&
(identical(other.name, name) || other.name == name) &&
(identical(other.imageUrl, imageUrl) ||
other.imageUrl == imageUrl));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, name, imageUrl);
/// Create a copy of Continent
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ContinentImplCopyWith<_$ContinentImpl> get copyWith =>
__$$ContinentImplCopyWithImpl<_$ContinentImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ContinentImplToJson(
this,
);
}
}
abstract class _Continent implements Continent {
const factory _Continent(
{required final String name,
required final String imageUrl}) = _$ContinentImpl;
factory _Continent.fromJson(Map<String, dynamic> json) =
_$ContinentImpl.fromJson;
/// e.g. 'Europe'
@override
String get name;
/// e.g. 'https://rstr.in/google/tripedia/TmR12QdlVTT'
@override
String get imageUrl;
/// Create a copy of Continent
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ContinentImplCopyWith<_$ContinentImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'continent.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$ContinentImpl _$$ContinentImplFromJson(Map<String, dynamic> json) =>
_$ContinentImpl(
name: json['name'] as String,
imageUrl: json['imageUrl'] as String,
);
Map<String, dynamic> _$$ContinentImplToJson(_$ContinentImpl instance) =>
<String, dynamic>{
'name': instance.name,
'imageUrl': instance.imageUrl,
};

View File

@@ -0,0 +1,38 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'destination.freezed.dart';
part 'destination.g.dart';
@freezed
class Destination with _$Destination {
const factory Destination({
/// e.g. 'alaska'
required String ref,
/// e.g. 'Alaska'
required String name,
/// e.g. 'United States'
required String country,
/// e.g. 'North America'
required String continent,
/// e.g. 'Alaska is a haven for outdoor enthusiasts ...'
required String knownFor,
/// e.g. ['Mountain', 'Off-the-beaten-path', 'Wildlife watching']
required List<String> tags,
/// e.g. 'https://storage.googleapis.com/tripedia-images/destinations/alaska.jpg'
required String imageUrl,
}) = _Destination;
factory Destination.fromJson(Map<String, Object?> json) =>
_$DestinationFromJson(json);
}

View File

@@ -0,0 +1,339 @@
// 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 'destination.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');
Destination _$DestinationFromJson(Map<String, dynamic> json) {
return _Destination.fromJson(json);
}
/// @nodoc
mixin _$Destination {
/// e.g. 'alaska'
String get ref => throw _privateConstructorUsedError;
/// e.g. 'Alaska'
String get name => throw _privateConstructorUsedError;
/// e.g. 'United States'
String get country => throw _privateConstructorUsedError;
/// e.g. 'North America'
String get continent => throw _privateConstructorUsedError;
/// e.g. 'Alaska is a haven for outdoor enthusiasts ...'
String get knownFor => throw _privateConstructorUsedError;
/// e.g. ['Mountain', 'Off-the-beaten-path', 'Wildlife watching']
List<String> get tags => throw _privateConstructorUsedError;
/// e.g. 'https://storage.googleapis.com/tripedia-images/destinations/alaska.jpg'
String get imageUrl => throw _privateConstructorUsedError;
/// Serializes this Destination to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of Destination
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$DestinationCopyWith<Destination> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DestinationCopyWith<$Res> {
factory $DestinationCopyWith(
Destination value, $Res Function(Destination) then) =
_$DestinationCopyWithImpl<$Res, Destination>;
@useResult
$Res call(
{String ref,
String name,
String country,
String continent,
String knownFor,
List<String> tags,
String imageUrl});
}
/// @nodoc
class _$DestinationCopyWithImpl<$Res, $Val extends Destination>
implements $DestinationCopyWith<$Res> {
_$DestinationCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Destination
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? ref = null,
Object? name = null,
Object? country = null,
Object? continent = null,
Object? knownFor = null,
Object? tags = null,
Object? imageUrl = null,
}) {
return _then(_value.copyWith(
ref: null == ref
? _value.ref
: ref // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
country: null == country
? _value.country
: country // ignore: cast_nullable_to_non_nullable
as String,
continent: null == continent
? _value.continent
: continent // ignore: cast_nullable_to_non_nullable
as String,
knownFor: null == knownFor
? _value.knownFor
: knownFor // ignore: cast_nullable_to_non_nullable
as String,
tags: null == tags
? _value.tags
: tags // ignore: cast_nullable_to_non_nullable
as List<String>,
imageUrl: null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$DestinationImplCopyWith<$Res>
implements $DestinationCopyWith<$Res> {
factory _$$DestinationImplCopyWith(
_$DestinationImpl value, $Res Function(_$DestinationImpl) then) =
__$$DestinationImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String ref,
String name,
String country,
String continent,
String knownFor,
List<String> tags,
String imageUrl});
}
/// @nodoc
class __$$DestinationImplCopyWithImpl<$Res>
extends _$DestinationCopyWithImpl<$Res, _$DestinationImpl>
implements _$$DestinationImplCopyWith<$Res> {
__$$DestinationImplCopyWithImpl(
_$DestinationImpl _value, $Res Function(_$DestinationImpl) _then)
: super(_value, _then);
/// Create a copy of Destination
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? ref = null,
Object? name = null,
Object? country = null,
Object? continent = null,
Object? knownFor = null,
Object? tags = null,
Object? imageUrl = null,
}) {
return _then(_$DestinationImpl(
ref: null == ref
? _value.ref
: ref // ignore: cast_nullable_to_non_nullable
as String,
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
country: null == country
? _value.country
: country // ignore: cast_nullable_to_non_nullable
as String,
continent: null == continent
? _value.continent
: continent // ignore: cast_nullable_to_non_nullable
as String,
knownFor: null == knownFor
? _value.knownFor
: knownFor // ignore: cast_nullable_to_non_nullable
as String,
tags: null == tags
? _value._tags
: tags // ignore: cast_nullable_to_non_nullable
as List<String>,
imageUrl: null == imageUrl
? _value.imageUrl
: imageUrl // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$DestinationImpl implements _Destination {
const _$DestinationImpl(
{required this.ref,
required this.name,
required this.country,
required this.continent,
required this.knownFor,
required final List<String> tags,
required this.imageUrl})
: _tags = tags;
factory _$DestinationImpl.fromJson(Map<String, dynamic> json) =>
_$$DestinationImplFromJson(json);
/// e.g. 'alaska'
@override
final String ref;
/// e.g. 'Alaska'
@override
final String name;
/// e.g. 'United States'
@override
final String country;
/// e.g. 'North America'
@override
final String continent;
/// e.g. 'Alaska is a haven for outdoor enthusiasts ...'
@override
final String knownFor;
/// e.g. ['Mountain', 'Off-the-beaten-path', 'Wildlife watching']
final List<String> _tags;
/// e.g. ['Mountain', 'Off-the-beaten-path', 'Wildlife watching']
@override
List<String> get tags {
if (_tags is EqualUnmodifiableListView) return _tags;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_tags);
}
/// e.g. 'https://storage.googleapis.com/tripedia-images/destinations/alaska.jpg'
@override
final String imageUrl;
@override
String toString() {
return 'Destination(ref: $ref, name: $name, country: $country, continent: $continent, knownFor: $knownFor, tags: $tags, imageUrl: $imageUrl)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$DestinationImpl &&
(identical(other.ref, ref) || other.ref == ref) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.country, country) || other.country == country) &&
(identical(other.continent, continent) ||
other.continent == continent) &&
(identical(other.knownFor, knownFor) ||
other.knownFor == knownFor) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
(identical(other.imageUrl, imageUrl) ||
other.imageUrl == imageUrl));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, ref, name, country, continent,
knownFor, const DeepCollectionEquality().hash(_tags), imageUrl);
/// Create a copy of Destination
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$DestinationImplCopyWith<_$DestinationImpl> get copyWith =>
__$$DestinationImplCopyWithImpl<_$DestinationImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$DestinationImplToJson(
this,
);
}
}
abstract class _Destination implements Destination {
const factory _Destination(
{required final String ref,
required final String name,
required final String country,
required final String continent,
required final String knownFor,
required final List<String> tags,
required final String imageUrl}) = _$DestinationImpl;
factory _Destination.fromJson(Map<String, dynamic> json) =
_$DestinationImpl.fromJson;
/// e.g. 'alaska'
@override
String get ref;
/// e.g. 'Alaska'
@override
String get name;
/// e.g. 'United States'
@override
String get country;
/// e.g. 'North America'
@override
String get continent;
/// e.g. 'Alaska is a haven for outdoor enthusiasts ...'
@override
String get knownFor;
/// e.g. ['Mountain', 'Off-the-beaten-path', 'Wildlife watching']
@override
List<String> get tags;
/// e.g. 'https://storage.googleapis.com/tripedia-images/destinations/alaska.jpg'
@override
String get imageUrl;
/// Create a copy of Destination
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$DestinationImplCopyWith<_$DestinationImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'destination.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$DestinationImpl _$$DestinationImplFromJson(Map<String, dynamic> json) =>
_$DestinationImpl(
ref: json['ref'] as String,
name: json['name'] as String,
country: json['country'] as String,
continent: json['continent'] as String,
knownFor: json['knownFor'] as String,
tags: (json['tags'] as List<dynamic>).map((e) => e as String).toList(),
imageUrl: json['imageUrl'] as String,
);
Map<String, dynamic> _$$DestinationImplToJson(_$DestinationImpl instance) =>
<String, dynamic>{
'ref': instance.ref,
'name': instance.name,
'country': instance.country,
'continent': instance.continent,
'knownFor': instance.knownFor,
'tags': instance.tags,
'imageUrl': instance.imageUrl,
};

View File

@@ -0,0 +1,35 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'itinerary_config.freezed.dart';
part 'itinerary_config.g.dart';
@freezed
class ItineraryConfig with _$ItineraryConfig {
const factory ItineraryConfig({
/// [Continent] name
String? continent,
/// Start date (check in) of itinerary
DateTime? startDate,
/// End date (check out) of itinerary
DateTime? endDate,
/// Number of guests
int? guests,
/// Selected [Destination] reference
String? destination,
/// Selected [Activity] references
@Default([]) List<String> activities,
}) = _ItineraryConfig;
factory ItineraryConfig.fromJson(Map<String, Object?> json) =>
_$ItineraryConfigFromJson(json);
}

View File

@@ -0,0 +1,316 @@
// 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 'itinerary_config.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');
ItineraryConfig _$ItineraryConfigFromJson(Map<String, dynamic> json) {
return _ItineraryConfig.fromJson(json);
}
/// @nodoc
mixin _$ItineraryConfig {
/// [Continent] name
String? get continent => throw _privateConstructorUsedError;
/// Start date (check in) of itinerary
DateTime? get startDate => throw _privateConstructorUsedError;
/// End date (check out) of itinerary
DateTime? get endDate => throw _privateConstructorUsedError;
/// Number of guests
int? get guests => throw _privateConstructorUsedError;
/// Selected [Destination] reference
String? get destination => throw _privateConstructorUsedError;
/// Selected [Activity] references
List<String> get activities => throw _privateConstructorUsedError;
/// Serializes this ItineraryConfig to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of ItineraryConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ItineraryConfigCopyWith<ItineraryConfig> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ItineraryConfigCopyWith<$Res> {
factory $ItineraryConfigCopyWith(
ItineraryConfig value, $Res Function(ItineraryConfig) then) =
_$ItineraryConfigCopyWithImpl<$Res, ItineraryConfig>;
@useResult
$Res call(
{String? continent,
DateTime? startDate,
DateTime? endDate,
int? guests,
String? destination,
List<String> activities});
}
/// @nodoc
class _$ItineraryConfigCopyWithImpl<$Res, $Val extends ItineraryConfig>
implements $ItineraryConfigCopyWith<$Res> {
_$ItineraryConfigCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ItineraryConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? continent = freezed,
Object? startDate = freezed,
Object? endDate = freezed,
Object? guests = freezed,
Object? destination = freezed,
Object? activities = null,
}) {
return _then(_value.copyWith(
continent: freezed == continent
? _value.continent
: continent // ignore: cast_nullable_to_non_nullable
as String?,
startDate: freezed == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
endDate: freezed == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
guests: freezed == guests
? _value.guests
: guests // ignore: cast_nullable_to_non_nullable
as int?,
destination: freezed == destination
? _value.destination
: destination // ignore: cast_nullable_to_non_nullable
as String?,
activities: null == activities
? _value.activities
: activities // ignore: cast_nullable_to_non_nullable
as List<String>,
) as $Val);
}
}
/// @nodoc
abstract class _$$ItineraryConfigImplCopyWith<$Res>
implements $ItineraryConfigCopyWith<$Res> {
factory _$$ItineraryConfigImplCopyWith(_$ItineraryConfigImpl value,
$Res Function(_$ItineraryConfigImpl) then) =
__$$ItineraryConfigImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String? continent,
DateTime? startDate,
DateTime? endDate,
int? guests,
String? destination,
List<String> activities});
}
/// @nodoc
class __$$ItineraryConfigImplCopyWithImpl<$Res>
extends _$ItineraryConfigCopyWithImpl<$Res, _$ItineraryConfigImpl>
implements _$$ItineraryConfigImplCopyWith<$Res> {
__$$ItineraryConfigImplCopyWithImpl(
_$ItineraryConfigImpl _value, $Res Function(_$ItineraryConfigImpl) _then)
: super(_value, _then);
/// Create a copy of ItineraryConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? continent = freezed,
Object? startDate = freezed,
Object? endDate = freezed,
Object? guests = freezed,
Object? destination = freezed,
Object? activities = null,
}) {
return _then(_$ItineraryConfigImpl(
continent: freezed == continent
? _value.continent
: continent // ignore: cast_nullable_to_non_nullable
as String?,
startDate: freezed == startDate
? _value.startDate
: startDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
endDate: freezed == endDate
? _value.endDate
: endDate // ignore: cast_nullable_to_non_nullable
as DateTime?,
guests: freezed == guests
? _value.guests
: guests // ignore: cast_nullable_to_non_nullable
as int?,
destination: freezed == destination
? _value.destination
: destination // ignore: cast_nullable_to_non_nullable
as String?,
activities: null == activities
? _value._activities
: activities // ignore: cast_nullable_to_non_nullable
as List<String>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ItineraryConfigImpl implements _ItineraryConfig {
const _$ItineraryConfigImpl(
{this.continent,
this.startDate,
this.endDate,
this.guests,
this.destination,
final List<String> activities = const []})
: _activities = activities;
factory _$ItineraryConfigImpl.fromJson(Map<String, dynamic> json) =>
_$$ItineraryConfigImplFromJson(json);
/// [Continent] name
@override
final String? continent;
/// Start date (check in) of itinerary
@override
final DateTime? startDate;
/// End date (check out) of itinerary
@override
final DateTime? endDate;
/// Number of guests
@override
final int? guests;
/// Selected [Destination] reference
@override
final String? destination;
/// Selected [Activity] references
final List<String> _activities;
/// Selected [Activity] references
@override
@JsonKey()
List<String> get activities {
if (_activities is EqualUnmodifiableListView) return _activities;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_activities);
}
@override
String toString() {
return 'ItineraryConfig(continent: $continent, startDate: $startDate, endDate: $endDate, guests: $guests, destination: $destination, activities: $activities)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ItineraryConfigImpl &&
(identical(other.continent, continent) ||
other.continent == continent) &&
(identical(other.startDate, startDate) ||
other.startDate == startDate) &&
(identical(other.endDate, endDate) || other.endDate == endDate) &&
(identical(other.guests, guests) || other.guests == guests) &&
(identical(other.destination, destination) ||
other.destination == destination) &&
const DeepCollectionEquality()
.equals(other._activities, _activities));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, continent, startDate, endDate,
guests, destination, const DeepCollectionEquality().hash(_activities));
/// Create a copy of ItineraryConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ItineraryConfigImplCopyWith<_$ItineraryConfigImpl> get copyWith =>
__$$ItineraryConfigImplCopyWithImpl<_$ItineraryConfigImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ItineraryConfigImplToJson(
this,
);
}
}
abstract class _ItineraryConfig implements ItineraryConfig {
const factory _ItineraryConfig(
{final String? continent,
final DateTime? startDate,
final DateTime? endDate,
final int? guests,
final String? destination,
final List<String> activities}) = _$ItineraryConfigImpl;
factory _ItineraryConfig.fromJson(Map<String, dynamic> json) =
_$ItineraryConfigImpl.fromJson;
/// [Continent] name
@override
String? get continent;
/// Start date (check in) of itinerary
@override
DateTime? get startDate;
/// End date (check out) of itinerary
@override
DateTime? get endDate;
/// Number of guests
@override
int? get guests;
/// Selected [Destination] reference
@override
String? get destination;
/// Selected [Activity] references
@override
List<String> get activities;
/// Create a copy of ItineraryConfig
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ItineraryConfigImplCopyWith<_$ItineraryConfigImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,36 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'itinerary_config.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$ItineraryConfigImpl _$$ItineraryConfigImplFromJson(
Map<String, dynamic> json) =>
_$ItineraryConfigImpl(
continent: json['continent'] as String?,
startDate: json['startDate'] == null
? null
: DateTime.parse(json['startDate'] as String),
endDate: json['endDate'] == null
? null
: DateTime.parse(json['endDate'] as String),
guests: (json['guests'] as num?)?.toInt(),
destination: json['destination'] as String?,
activities: (json['activities'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
);
Map<String, dynamic> _$$ItineraryConfigImplToJson(
_$ItineraryConfigImpl instance) =>
<String, dynamic>{
'continent': instance.continent,
'startDate': instance.startDate?.toIso8601String(),
'endDate': instance.endDate?.toIso8601String(),
'guests': instance.guests,
'destination': instance.destination,
'activities': instance.activities,
};

View File

@@ -0,0 +1,21 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
abstract class User with _$User {
const factory User({
/// The user's name.
required String name,
/// The user's picture URL.
required String picture,
}) = _User;
factory User.fromJson(Map<String, Object?> json) => _$UserFromJson(json);
}

View File

@@ -0,0 +1,185 @@
// 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 'user.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');
User _$UserFromJson(Map<String, dynamic> json) {
return _User.fromJson(json);
}
/// @nodoc
mixin _$User {
/// The user's name.
String get name => throw _privateConstructorUsedError;
/// The user's picture URL.
String get picture => throw _privateConstructorUsedError;
/// Serializes this User to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$UserCopyWith<User> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $UserCopyWith<$Res> {
factory $UserCopyWith(User value, $Res Function(User) then) =
_$UserCopyWithImpl<$Res, User>;
@useResult
$Res call({String name, String picture});
}
/// @nodoc
class _$UserCopyWithImpl<$Res, $Val extends User>
implements $UserCopyWith<$Res> {
_$UserCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? picture = null,
}) {
return _then(_value.copyWith(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
picture: null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
) as $Val);
}
}
/// @nodoc
abstract class _$$UserImplCopyWith<$Res> implements $UserCopyWith<$Res> {
factory _$$UserImplCopyWith(
_$UserImpl value, $Res Function(_$UserImpl) then) =
__$$UserImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String name, String picture});
}
/// @nodoc
class __$$UserImplCopyWithImpl<$Res>
extends _$UserCopyWithImpl<$Res, _$UserImpl>
implements _$$UserImplCopyWith<$Res> {
__$$UserImplCopyWithImpl(_$UserImpl _value, $Res Function(_$UserImpl) _then)
: super(_value, _then);
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? picture = null,
}) {
return _then(_$UserImpl(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
picture: null == picture
? _value.picture
: picture // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _$UserImpl implements _User {
const _$UserImpl({required this.name, required this.picture});
factory _$UserImpl.fromJson(Map<String, dynamic> json) =>
_$$UserImplFromJson(json);
/// The user's name.
@override
final String name;
/// The user's picture URL.
@override
final String picture;
@override
String toString() {
return 'User(name: $name, picture: $picture)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UserImpl &&
(identical(other.name, name) || other.name == name) &&
(identical(other.picture, picture) || other.picture == picture));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, name, picture);
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$UserImplCopyWith<_$UserImpl> get copyWith =>
__$$UserImplCopyWithImpl<_$UserImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$UserImplToJson(
this,
);
}
}
abstract class _User implements User {
const factory _User(
{required final String name, required final String picture}) = _$UserImpl;
factory _User.fromJson(Map<String, dynamic> json) = _$UserImpl.fromJson;
/// The user's name.
@override
String get name;
/// The user's picture URL.
@override
String get picture;
/// Create a copy of User
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$UserImplCopyWith<_$UserImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -0,0 +1,18 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$UserImpl _$$UserImplFromJson(Map<String, dynamic> json) => _$UserImpl(
name: json['name'] as String,
picture: json['picture'] as String,
);
Map<String, dynamic> _$$UserImplToJson(_$UserImpl instance) =>
<String, dynamic>{
'name': instance.name,
'picture': instance.picture,
};

View File

@@ -0,0 +1,106 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:logging/logging.dart';
import '../../../data/repositories/activity/activity_repository.dart';
import '../../../data/repositories/booking/booking_repository.dart';
import '../../../data/repositories/destination/destination_repository.dart';
import '../../../utils/result.dart';
import '../../models/activity/activity.dart';
import '../../models/booking/booking.dart';
import '../../models/destination/destination.dart';
import '../../models/itinerary_config/itinerary_config.dart';
/// UseCase for creating [Booking] objects from [ItineraryConfig].
///
/// Fetches [Destination] and [Activity] objects from repositories,
/// checks if dates are set and creates a [Booking] object.
class BookingCreateUseCase {
BookingCreateUseCase({
required DestinationRepository destinationRepository,
required ActivityRepository activityRepository,
required BookingRepository bookingRepository,
}) : _destinationRepository = destinationRepository,
_activityRepository = activityRepository,
_bookingRepository = bookingRepository;
final DestinationRepository _destinationRepository;
final ActivityRepository _activityRepository;
final BookingRepository _bookingRepository;
final _log = Logger('BookingCreateUseCase');
/// 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'));
}
final booking = Booking(
startDate: itineraryConfig.startDate!,
endDate: itineraryConfig.endDate!,
destination: destinationResult.asOk.value,
activity: activities,
);
final saveBookingResult = await _bookingRepository.createBooking(booking);
switch (saveBookingResult) {
case Ok<void>():
_log.fine('Booking saved successfully');
break;
case Error<void>():
_log.warning('Failed to save booking', saveBookingResult.error);
return Result.error(saveBookingResult.error);
}
// Create Booking object
return Result.ok(booking);
}
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);
}
}
}

View File

@@ -0,0 +1,46 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:share_plus/share_plus.dart';
import '../../../utils/result.dart';
import '../../../ui/core/ui/date_format_start_end.dart';
import '../../models/booking/booking.dart';
typedef ShareFunction = Future<void> Function(String text);
/// UseCase for sharing a booking.
class BookingShareUseCase {
BookingShareUseCase._(this._share);
/// Create a [BookingShareUseCase] that uses `share_plus` package.
factory BookingShareUseCase.withSharePlus() =>
BookingShareUseCase._(Share.share);
/// Create a [BookingShareUseCase] with a custom share function.
factory BookingShareUseCase.custom(ShareFunction share) =>
BookingShareUseCase._(share);
final ShareFunction _share;
final _log = Logger('BookingShareUseCase');
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);
}
}
}

View File

@@ -0,0 +1,40 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'ui/core/localization/applocalization.dart';
import 'ui/core/themes/theme.dart';
import 'routing/router.dart';
import 'package:flutter/material.dart';
import 'ui/core/ui/scroll_behavior.dart';
import 'main_development.dart' as development;
/// Default main method
void main() {
// Launch development config by default
development.main();
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
localizationsDelegates: [
GlobalWidgetsLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
AppLocalizationDelegate(),
],
scrollBehavior: AppCustomScrollBehavior(),
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
routerConfig: router(context.read()),
);
}
}

View File

@@ -0,0 +1,24 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'config/dependencies.dart';
import 'main.dart';
/// Development config entry point.
/// Launch with `flutter run --target lib/main_development.dart`.
/// Uses local data.
void main() {
Logger.root.level = Level.ALL;
runApp(
MultiProvider(
providers: providersLocal,
child: const MainApp(),
),
);
}

View File

@@ -0,0 +1,24 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'config/dependencies.dart';
import 'main.dart';
/// Staging config entry point.
/// Launch with `flutter run --target lib/main_staging.dart`.
/// Uses remote data from a server.
void main() {
Logger.root.level = Level.ALL;
runApp(
MultiProvider(
providers: providersRemote,
child: const MainApp(),
),
);
}

View File

@@ -0,0 +1,154 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import '../data/repositories/auth/auth_repository.dart';
import '../ui/activities/view_models/activities_viewmodel.dart';
import '../ui/activities/widgets/activities_screen.dart';
import '../ui/auth/login/view_models/login_viewmodel.dart';
import '../ui/auth/login/widgets/login_screen.dart';
import '../ui/booking/widgets/booking_screen.dart';
import '../ui/booking/view_models/booking_viewmodel.dart';
import '../ui/home/view_models/home_viewmodel.dart';
import '../ui/home/widgets/home_screen.dart';
import '../ui/results/view_models/results_viewmodel.dart';
import '../ui/results/widgets/results_screen.dart';
import '../ui/search_form/view_models/search_form_viewmodel.dart';
import '../ui/search_form/widgets/search_form_screen.dart';
import 'routes.dart';
/// Top go_router entry point.
///
/// Listens to changes in [AuthTokenRepository] to redirect the user
/// to /login when the user logs out.
GoRouter router(
AuthRepository authRepository,
) =>
GoRouter(
initialLocation: Routes.home,
debugLogDiagnostics: true,
redirect: _redirect,
refreshListenable: authRepository,
routes: [
GoRoute(
path: Routes.login,
builder: (context, state) {
return LoginScreen(
viewModel: LoginViewModel(
authRepository: context.read(),
),
);
},
),
GoRoute(
path: Routes.home,
builder: (context, state) {
final viewModel = HomeViewModel(
bookingRepository: context.read(),
userRepository: context.read(),
);
return HomeScreen(viewModel: viewModel);
},
routes: [
GoRoute(
path: Routes.searchRelative,
builder: (context, state) {
final viewModel = SearchFormViewModel(
continentRepository: context.read(),
itineraryConfigRepository: context.read(),
);
return SearchFormScreen(viewModel: viewModel);
},
),
GoRoute(
path: Routes.resultsRelative,
builder: (context, state) {
final viewModel = ResultsViewModel(
destinationRepository: context.read(),
itineraryConfigRepository: context.read(),
);
return ResultsScreen(
viewModel: viewModel,
);
},
),
GoRoute(
path: Routes.activitiesRelative,
builder: (context, state) {
final viewModel = ActivitiesViewModel(
activityRepository: context.read(),
itineraryConfigRepository: context.read(),
);
return ActivitiesScreen(
viewModel: viewModel,
);
},
),
GoRoute(
path: Routes.bookingRelative,
builder: (context, state) {
final viewModel = BookingViewModel(
itineraryConfigRepository: context.read(),
createBookingUseCase: context.read(),
shareBookingUseCase: context.read(),
bookingRepository: context.read(),
);
// When opening the booking screen directly
// create a new booking from the stored ItineraryConfig.
viewModel.createBooking.execute();
return BookingScreen(
viewModel: viewModel,
);
},
routes: [
GoRoute(
path: ':id',
builder: (context, state) {
final id = int.parse(state.pathParameters['id']!);
final viewModel = BookingViewModel(
itineraryConfigRepository: context.read(),
createBookingUseCase: context.read(),
shareBookingUseCase: context.read(),
bookingRepository: context.read(),
);
// When opening the booking screen with an existing id
// load and display that booking.
viewModel.loadBooking.execute(id);
return BookingScreen(
viewModel: viewModel,
);
},
),
],
),
],
),
],
);
// From https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/redirection.dart
Future<String?> _redirect(BuildContext context, GoRouterState state) async {
// if the user is not logged in, they need to login
final bool loggedIn = await context.read<AuthRepository>().isAuthenticated;
final bool loggingIn = state.matchedLocation == Routes.login;
if (!loggedIn) {
return Routes.login;
}
// if the user is logged in but still on the login page, send them to
// the home page
if (loggingIn) {
return Routes.home;
}
// no need to redirect at all
return null;
}

View File

@@ -0,0 +1,17 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
class Routes {
static const home = '/';
static const login = '/login';
static const search = '/$searchRelative';
static const searchRelative = 'search';
static const results = '/$resultsRelative';
static const resultsRelative = 'results';
static const activities = '/$activitiesRelative';
static const activitiesRelative = 'activities';
static const booking = '/$bookingRelative';
static const bookingRelative = 'booking';
static String bookingWithId(int id) => '$booking/$id';
}

View File

@@ -0,0 +1,142 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import '../../../data/repositories/activity/activity_repository.dart';
import '../../../data/repositories/itinerary_config/itinerary_config_repository.dart';
import '../../../domain/models/activity/activity.dart';
import '../../../utils/command.dart';
import '../../../utils/result.dart';
class ActivitiesViewModel extends ChangeNotifier {
ActivitiesViewModel({
required ActivityRepository activityRepository,
required ItineraryConfigRepository itineraryConfigRepository,
}) : _activityRepository = activityRepository,
_itineraryConfigRepository = itineraryConfigRepository {
loadActivities = Command0(_loadActivities)..execute();
saveActivities = Command0(_saveActivities);
}
final _log = Logger('ActivitiesViewModel');
final ActivityRepository _activityRepository;
final ItineraryConfigRepository _itineraryConfigRepository;
List<Activity> _daytimeActivities = <Activity>[];
List<Activity> _eveningActivities = <Activity>[];
final Set<String> _selectedActivities = <String>{};
/// List of daytime [Activity] per destination.
List<Activity> get daytimeActivities => _daytimeActivities;
/// List of evening [Activity] per destination.
List<Activity> get eveningActivities => _eveningActivities;
/// Selected [Activity] by ref.
Set<String> get selectedActivities => _selectedActivities;
/// Load list of [Activity] for a [Destination] by ref.
late final Command0 loadActivities;
/// Save list [selectedActivities] into itinerary configuration.
late final Command0 saveActivities;
Future<Result<void>> _loadActivities() async {
final result = await _itineraryConfigRepository.getItineraryConfig();
if (result is Error) {
_log.warning(
'Failed to load stored ItineraryConfig',
result.asError.error,
);
return result;
}
final destinationRef = result.asOk.value.destination;
if (destinationRef == null) {
_log.severe('Destination missing in ItineraryConfig');
return Result.error(Exception('Destination not found'));
}
_selectedActivities.addAll(result.asOk.value.activities);
final resultActivities =
await _activityRepository.getByDestination(destinationRef);
switch (resultActivities) {
case Ok():
{
_daytimeActivities = resultActivities.value
.where((activity) => [
TimeOfDay.any,
TimeOfDay.morning,
TimeOfDay.afternoon,
].contains(activity.timeOfDay))
.toList();
_eveningActivities = resultActivities.value
.where((activity) => [
TimeOfDay.evening,
TimeOfDay.night,
].contains(activity.timeOfDay))
.toList();
_log.fine('Activities (daytime: ${_daytimeActivities.length}, '
'evening: ${_eveningActivities.length}) loaded');
}
case Error():
{
_log.warning('Failed to load activities', resultActivities.error);
}
}
notifyListeners();
return resultActivities;
}
/// Add [Activity] to selected list.
void addActivity(String activityRef) {
assert(
(_daytimeActivities + _eveningActivities)
.any((activity) => activity.ref == activityRef),
"Activity $activityRef not found",
);
_selectedActivities.add(activityRef);
_log.finest('Activity $activityRef added');
notifyListeners();
}
/// Remove [Activity] from selected list.
void removeActivity(String activityRef) {
assert(
(_daytimeActivities + _eveningActivities)
.any((activity) => activity.ref == activityRef),
"Activity $activityRef not found",
);
_selectedActivities.remove(activityRef);
_log.finest('Activity $activityRef removed');
notifyListeners();
}
Future<Result<void>> _saveActivities() async {
final resultConfig = await _itineraryConfigRepository.getItineraryConfig();
if (resultConfig is Error) {
_log.warning(
'Failed to load stored ItineraryConfig',
resultConfig.asError.error,
);
return resultConfig;
}
final itineraryConfig = resultConfig.asOk.value;
final result = await _itineraryConfigRepository.setItineraryConfig(
itineraryConfig.copyWith(activities: _selectedActivities.toList()));
if (result is Error) {
_log.warning(
'Failed to store ItineraryConfig',
result.asError.error,
);
}
return result;
}
}

View File

@@ -0,0 +1,48 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../routing/routes.dart';
import '../../core/localization/applocalization.dart';
import '../../core/themes/dimens.dart';
import '../../core/ui/back_button.dart';
import '../../core/ui/home_button.dart';
class ActivitiesHeader extends StatelessWidget {
const ActivitiesHeader({super.key});
@override
Widget build(BuildContext context) {
return SafeArea(
top: true,
bottom: false,
child: Padding(
padding: EdgeInsets.only(
left: Dimens.of(context).paddingScreenHorizontal,
right: Dimens.of(context).paddingScreenHorizontal,
top: Dimens.of(context).paddingScreenVertical,
bottom: Dimens.paddingVertical,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomBackButton(
onTap: () {
// Navigate to ResultsScreen and edit search
context.go(Routes.results);
},
),
Text(
AppLocalization.of(context).activities,
style: Theme.of(context).textTheme.titleLarge,
),
const HomeButton(),
],
),
),
);
}
}

View File

@@ -0,0 +1,61 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../core/themes/dimens.dart';
import '../view_models/activities_viewmodel.dart';
import 'activity_entry.dart';
import 'activity_time_of_day.dart';
class ActivitiesList extends StatelessWidget {
const ActivitiesList({
super.key,
required this.viewModel,
required this.activityTimeOfDay,
});
final ActivitiesViewModel viewModel;
final ActivityTimeOfDay activityTimeOfDay;
@override
Widget build(BuildContext context) {
final list = switch (activityTimeOfDay) {
ActivityTimeOfDay.daytime => viewModel.daytimeActivities,
ActivityTimeOfDay.evening => viewModel.eveningActivities,
};
return SliverPadding(
padding: EdgeInsets.only(
top: Dimens.paddingVertical,
left: Dimens.of(context).paddingScreenHorizontal,
right: Dimens.of(context).paddingScreenHorizontal,
bottom: Dimens.paddingVertical,
),
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
final activity = list[index];
return Padding(
padding:
EdgeInsets.only(bottom: index < list.length - 1 ? 20 : 0),
child: ActivityEntry(
key: ValueKey(activity.ref),
activity: activity,
selected: viewModel.selectedActivities.contains(activity.ref),
onChanged: (value) {
if (value!) {
viewModel.addActivity(activity.ref);
} else {
viewModel.removeActivity(activity.ref);
}
},
),
);
},
childCount: list.length,
),
),
);
}
}

View File

@@ -0,0 +1,188 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../routing/routes.dart';
import '../../core/localization/applocalization.dart';
import '../../core/themes/dimens.dart';
import '../../core/ui/error_indicator.dart';
import '../view_models/activities_viewmodel.dart';
import 'activities_header.dart';
import 'activities_list.dart';
import 'activities_title.dart';
import 'activity_time_of_day.dart';
const String confirmButtonKey = 'confirm-button';
class ActivitiesScreen extends StatefulWidget {
const ActivitiesScreen({
super.key,
required this.viewModel,
});
final ActivitiesViewModel viewModel;
@override
State<ActivitiesScreen> createState() => _ActivitiesScreenState();
}
class _ActivitiesScreenState extends State<ActivitiesScreen> {
@override
void initState() {
super.initState();
widget.viewModel.saveActivities.addListener(_onResult);
}
@override
void didUpdateWidget(covariant ActivitiesScreen oldWidget) {
super.didUpdateWidget(oldWidget);
oldWidget.viewModel.saveActivities.removeListener(_onResult);
widget.viewModel.saveActivities.addListener(_onResult);
}
@override
void dispose() {
widget.viewModel.saveActivities.removeListener(_onResult);
super.dispose();
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, r) {
if (!didPop) context.go(Routes.results);
},
child: Scaffold(
body: ListenableBuilder(
listenable: widget.viewModel.loadActivities,
builder: (context, child) {
if (widget.viewModel.loadActivities.completed) {
return child!;
}
return Column(
children: [
const ActivitiesHeader(),
if (widget.viewModel.loadActivities.running)
const Expanded(
child: Center(child: CircularProgressIndicator())),
if (widget.viewModel.loadActivities.error)
Expanded(
child: Center(
child: ErrorIndicator(
title: AppLocalization.of(context)
.errorWhileLoadingActivities,
label: AppLocalization.of(context).tryAgain,
onPressed: widget.viewModel.loadActivities.execute,
),
),
),
],
);
},
child: ListenableBuilder(
listenable: widget.viewModel,
builder: (context, child) {
return Column(
children: [
Expanded(
child: CustomScrollView(
slivers: [
const SliverToBoxAdapter(
child: ActivitiesHeader(),
),
ActivitiesTitle(
viewModel: widget.viewModel,
activityTimeOfDay: ActivityTimeOfDay.daytime,
),
ActivitiesList(
viewModel: widget.viewModel,
activityTimeOfDay: ActivityTimeOfDay.daytime,
),
ActivitiesTitle(
viewModel: widget.viewModel,
activityTimeOfDay: ActivityTimeOfDay.evening,
),
ActivitiesList(
viewModel: widget.viewModel,
activityTimeOfDay: ActivityTimeOfDay.evening,
),
],
),
),
_BottomArea(viewModel: widget.viewModel),
],
);
},
),
),
),
);
}
void _onResult() {
if (widget.viewModel.saveActivities.completed) {
widget.viewModel.saveActivities.clearResult();
context.go(Routes.booking);
}
if (widget.viewModel.saveActivities.error) {
widget.viewModel.saveActivities.clearResult();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalization.of(context).errorWhileSavingActivities),
action: SnackBarAction(
label: AppLocalization.of(context).tryAgain,
onPressed: widget.viewModel.saveActivities.execute,
),
),
);
}
}
}
class _BottomArea extends StatelessWidget {
const _BottomArea({
required this.viewModel,
});
final ActivitiesViewModel viewModel;
@override
Widget build(BuildContext context) {
return SafeArea(
bottom: true,
child: Material(
elevation: 8,
child: Padding(
padding: EdgeInsets.only(
left: Dimens.of(context).paddingScreenHorizontal,
right: Dimens.of(context).paddingScreenVertical,
top: Dimens.paddingVertical,
bottom: Dimens.of(context).paddingScreenVertical,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
AppLocalization.of(context)
.selected(viewModel.selectedActivities.length),
style: Theme.of(context).textTheme.labelLarge,
),
FilledButton(
key: const Key(confirmButtonKey),
onPressed: viewModel.selectedActivities.isNotEmpty
? viewModel.saveActivities.execute
: null,
child: Text(AppLocalization.of(context).confirm),
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,43 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../core/localization/applocalization.dart';
import '../../core/themes/dimens.dart';
import '../view_models/activities_viewmodel.dart';
import 'activity_time_of_day.dart';
class ActivitiesTitle extends StatelessWidget {
const ActivitiesTitle({
super.key,
required this.activityTimeOfDay,
required this.viewModel,
});
final ActivitiesViewModel viewModel;
final ActivityTimeOfDay activityTimeOfDay;
@override
Widget build(BuildContext context) {
final list = switch (activityTimeOfDay) {
ActivityTimeOfDay.daytime => viewModel.daytimeActivities,
ActivityTimeOfDay.evening => viewModel.eveningActivities,
};
if (list.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox());
}
return SliverToBoxAdapter(
child: Padding(
padding: Dimens.of(context).edgeInsetsScreenHorizontal,
child: Text(_label(context)),
),
);
}
String _label(BuildContext context) => switch (activityTimeOfDay) {
ActivityTimeOfDay.daytime => AppLocalization.of(context).daytime,
ActivityTimeOfDay.evening => AppLocalization.of(context).evening,
};
}

View File

@@ -0,0 +1,68 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../../../domain/models/activity/activity.dart';
import '../../../utils/image_error_listener.dart';
import '../../core/ui/custom_checkbox.dart';
class ActivityEntry extends StatelessWidget {
const ActivityEntry({
super.key,
required this.activity,
required this.selected,
required this.onChanged,
});
final Activity activity;
final bool selected;
final ValueChanged<bool?> onChanged;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 80,
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: activity.imageUrl,
height: 80,
width: 80,
errorListener: imageErrorListener,
),
),
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: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
const SizedBox(width: 20),
CustomCheckbox(
key: ValueKey('${activity.ref}-checkbox'),
value: selected,
onChanged: onChanged,
)
],
),
);
}
}

View File

@@ -0,0 +1,5 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
enum ActivityTimeOfDay { daytime, evening }

View File

@@ -0,0 +1,34 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:logging/logging.dart';
import '../../../../data/repositories/auth/auth_repository.dart';
import '../../../../utils/command.dart';
import '../../../../utils/result.dart';
class LoginViewModel {
LoginViewModel({
required AuthRepository authRepository,
}) : _authRepository = authRepository {
login = Command1<void, (String email, String password)>(_login);
}
final AuthRepository _authRepository;
final _log = Logger('LoginViewModel');
late Command1 login;
Future<Result<void>> _login((String, String) credentials) async {
final (email, password) = credentials;
final result = await _authRepository.login(
email: email,
password: password,
);
if (result is Error<void>) {
_log.warning('Login failed! ${result.error}');
}
return result;
}
}

View File

@@ -0,0 +1,113 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../../routing/routes.dart';
import '../../../core/localization/applocalization.dart';
import '../../../core/themes/dimens.dart';
import '../view_models/login_viewmodel.dart';
import 'tilted_cards.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({
super.key,
required this.viewModel,
});
final LoginViewModel viewModel;
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final TextEditingController _email =
TextEditingController(text: 'email@example.com');
final TextEditingController _password =
TextEditingController(text: 'password');
@override
void initState() {
super.initState();
widget.viewModel.login.addListener(_onResult);
}
@override
void didUpdateWidget(covariant LoginScreen oldWidget) {
super.didUpdateWidget(oldWidget);
oldWidget.viewModel.login.removeListener(_onResult);
widget.viewModel.login.addListener(_onResult);
}
@override
void dispose() {
widget.viewModel.login.removeListener(_onResult);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const TiltedCards(),
Padding(
padding: Dimens.of(context).edgeInsetsScreenSymmetric,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextField(
controller: _email,
),
const SizedBox(height: Dimens.paddingVertical),
TextField(
controller: _password,
obscureText: true,
),
const SizedBox(height: Dimens.paddingVertical),
ListenableBuilder(
listenable: widget.viewModel.login,
builder: (context, _) {
return FilledButton(
onPressed: () {
widget.viewModel.login
.execute((_email.value.text, _password.value.text));
},
child: Text(AppLocalization.of(context).login),
);
},
),
],
),
),
],
),
);
}
void _onResult() {
if (widget.viewModel.login.completed) {
widget.viewModel.login.clearResult();
context.go(Routes.home);
}
if (widget.viewModel.login.error) {
widget.viewModel.login.clearResult();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalization.of(context).errorWhileLogin),
action: SnackBarAction(
label: AppLocalization.of(context).tryAgain,
onPressed: () => widget.viewModel.login
.execute((_email.value.text, _password.value.text)),
),
),
);
}
}
}

View File

@@ -0,0 +1,97 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import '../../../../utils/image_error_listener.dart';
class TiltedCards extends StatelessWidget {
const TiltedCards({super.key});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 300),
child: const AspectRatio(
aspectRatio: 1,
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
left: 0,
child: _Card(
imageUrl: 'https://rstr.in/google/tripedia/g2i0BsYPKW-',
width: 200,
height: 273,
tilt: -3.83 / 360,
),
),
Positioned(
right: 0,
child: _Card(
imageUrl: 'https://rstr.in/google/tripedia/980sqNgaDRK',
width: 180,
height: 230,
tilt: 3.46 / 360,
),
),
_Card(
imageUrl: 'https://rstr.in/google/tripedia/pHfPmf3o5NU',
width: 225,
height: 322,
tilt: 0,
showTitle: true,
),
],
),
),
);
}
}
class _Card extends StatelessWidget {
const _Card({
required this.imageUrl,
required this.width,
required this.height,
required this.tilt,
this.showTitle = false,
});
final double tilt;
final double width;
final double height;
final String imageUrl;
final bool showTitle;
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: AlwaysStoppedAnimation(tilt),
child: SizedBox(
width: width,
height: height,
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
fit: StackFit.expand,
alignment: Alignment.center,
children: [
CachedNetworkImage(
imageUrl: imageUrl,
fit: BoxFit.cover,
color: showTitle ? Colors.black.withOpacity(0.5) : null,
colorBlendMode: showTitle ? BlendMode.darken : null,
errorListener: imageErrorListener,
),
if (showTitle) Center(child: SvgPicture.asset('assets/logo.svg')),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,34 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../../data/repositories/auth/auth_repository.dart';
import '../../../../data/repositories/itinerary_config/itinerary_config_repository.dart';
import '../../../../domain/models/itinerary_config/itinerary_config.dart';
import '../../../../utils/command.dart';
import '../../../../utils/result.dart';
class LogoutViewModel {
LogoutViewModel({
required AuthRepository authRepository,
required ItineraryConfigRepository itineraryConfigRepository,
}) : _authLogoutRepository = authRepository,
_itineraryConfigRepository = itineraryConfigRepository {
logout = Command0(_logout);
}
final AuthRepository _authLogoutRepository;
final ItineraryConfigRepository _itineraryConfigRepository;
late Command0 logout;
Future<Result> _logout() async {
var result = await _authLogoutRepository.logout();
switch (result) {
case Ok<void>():
// clear stored itinerary config
return _itineraryConfigRepository
.setItineraryConfig(const ItineraryConfig());
case Error<void>():
return result;
}
}
}

View File

@@ -0,0 +1,88 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../../core/localization/applocalization.dart';
import '../../../core/themes/colors.dart';
import '../view_models/logout_viewmodel.dart';
class LogoutButton extends StatefulWidget {
const LogoutButton({
super.key,
required this.viewModel,
});
final LogoutViewModel viewModel;
@override
State<LogoutButton> createState() => _LogoutButtonState();
}
class _LogoutButtonState extends State<LogoutButton> {
@override
void initState() {
super.initState();
widget.viewModel.logout.addListener(_onResult);
}
@override
void didUpdateWidget(covariant LogoutButton oldWidget) {
super.didUpdateWidget(oldWidget);
oldWidget.viewModel.logout.removeListener(_onResult);
widget.viewModel.logout.addListener(_onResult);
}
@override
void dispose() {
widget.viewModel.logout.removeListener(_onResult);
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40.0,
width: 40.0,
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey1),
borderRadius: BorderRadius.circular(8.0),
color: Colors.transparent,
),
child: InkResponse(
borderRadius: BorderRadius.circular(8.0),
onTap: () {
widget.viewModel.logout.execute();
},
child: Center(
child: Icon(
size: 24.0,
Icons.logout,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
);
}
void _onResult() {
// We do not need to navigate to `/login` on logout,
// it is done automatically by GoRouter.
if (widget.viewModel.logout.error) {
widget.viewModel.logout.clearResult();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalization.of(context).errorWhileLogout),
action: SnackBarAction(
label: AppLocalization.of(context).tryAgain,
onPressed: widget.viewModel.logout.execute,
),
),
);
}
}
}

View File

@@ -0,0 +1,89 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import '../../../data/repositories/booking/booking_repository.dart';
import '../../../data/repositories/itinerary_config/itinerary_config_repository.dart';
import '../../../domain/models/booking/booking.dart';
import '../../../domain/models/itinerary_config/itinerary_config.dart';
import '../../../utils/command.dart';
import '../../../utils/result.dart';
import '../../../domain/use_cases/booking/booking_create_use_case.dart';
import '../../../domain/use_cases/booking/booking_share_use_case.dart';
class BookingViewModel extends ChangeNotifier {
BookingViewModel({
required BookingCreateUseCase createBookingUseCase,
required BookingShareUseCase shareBookingUseCase,
required ItineraryConfigRepository itineraryConfigRepository,
required BookingRepository bookingRepository,
}) : _createUseCase = createBookingUseCase,
_shareUseCase = shareBookingUseCase,
_itineraryConfigRepository = itineraryConfigRepository,
_bookingRepository = bookingRepository {
createBooking = Command0(_createBooking);
shareBooking = Command0(() => _shareUseCase.shareBooking(_booking!));
loadBooking = Command1(_load);
}
final BookingCreateUseCase _createUseCase;
final BookingShareUseCase _shareUseCase;
final ItineraryConfigRepository _itineraryConfigRepository;
final BookingRepository _bookingRepository;
final _log = Logger('BookingViewModel');
Booking? _booking;
Booking? get booking => _booking;
/// Creates a booking from the ItineraryConfig
/// and saves it to the user bookins
late final Command0 createBooking;
/// Loads booking by id
late final Command1<void, int> loadBooking;
/// Share the current booking using the OS share dialog.
late final Command0 shareBooking;
Future<Result<void>> _createBooking() async {
_log.fine('Loading booking');
final itineraryConfig =
await _itineraryConfigRepository.getItineraryConfig();
switch (itineraryConfig) {
case Ok<ItineraryConfig>():
_log.fine('Loaded stored ItineraryConfig');
final result = await _createUseCase.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);
}
}
Future<Result<void>> _load(int id) async {
final result = await _bookingRepository.getBooking(id);
switch (result) {
case Ok<Booking>():
_log.fine('Loaded booking $id');
_booking = result.value;
notifyListeners();
case Error<Booking>():
_log.warning('Failed to load booking $id');
}
return result;
}
}

View File

@@ -0,0 +1,104 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../../../domain/models/activity/activity.dart';
import '../../../utils/image_error_listener.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,
errorListener: imageErrorListener,
),
),
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,
),
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,191 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../../../domain/models/booking/booking.dart';
import '../../../utils/image_error_listener.dart';
import '../../core/localization/applocalization.dart';
import '../../core/themes/colors.dart';
import '../../core/themes/dimens.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(
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,
errorListener: imageErrorListener,
);
}
}
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,
],
),
),
);
}
}

View File

@@ -0,0 +1,115 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../routing/routes.dart';
import '../../core/localization/applocalization.dart';
import '../../core/ui/error_indicator.dart';
import '../view_models/booking_viewmodel.dart';
import 'booking_body.dart';
class BookingScreen extends StatefulWidget {
const BookingScreen({
super.key,
required this.viewModel,
});
final BookingViewModel viewModel;
@override
State<BookingScreen> createState() => _BookingScreenState();
}
class _BookingScreenState extends State<BookingScreen> {
@override
void initState() {
super.initState();
widget.viewModel.shareBooking.addListener(_listener);
}
@override
void dispose() {
widget.viewModel.shareBooking.removeListener(_listener);
super.dispose();
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, r) {
// Back navigation always goes to home
if (!didPop) context.go(Routes.home);
},
child: Scaffold(
floatingActionButton: ListenableBuilder(
listenable: widget.viewModel,
builder: (context, _) => FloatingActionButton.extended(
// Workaround for https://github.com/flutter/flutter/issues/115358#issuecomment-2117157419
heroTag: null,
key: const ValueKey('share-button'),
onPressed: widget.viewModel.booking != null
? widget.viewModel.shareBooking.execute
: null,
label: Text(AppLocalization.of(context).shareTrip),
icon: const Icon(Icons.share_outlined),
),
),
body: ListenableBuilder(
// Listen to changes in both commands
listenable: Listenable.merge([
widget.viewModel.createBooking,
widget.viewModel.loadBooking,
]),
builder: (context, child) {
// If either command is running, show progress indicator
if (widget.viewModel.createBooking.running ||
widget.viewModel.loadBooking.running) {
return const Center(
child: CircularProgressIndicator(),
);
}
// If fails to create booking, tap to try again
if (widget.viewModel.createBooking.error) {
return Center(
child: ErrorIndicator(
title: AppLocalization.of(context).errorWhileLoadingBooking,
label: AppLocalization.of(context).tryAgain,
onPressed: widget.viewModel.createBooking.execute,
),
);
}
// If existing booking fails to load, tap to go /home
if (widget.viewModel.loadBooking.error) {
return Center(
child: ErrorIndicator(
title: AppLocalization.of(context).errorWhileLoadingBooking,
label: AppLocalization.of(context).close,
onPressed: () => context.go(Routes.home),
),
);
}
return child!;
},
child: BookingBody(viewModel: widget.viewModel),
),
),
);
}
void _listener() {
if (widget.viewModel.shareBooking.error) {
widget.viewModel.shareBooking.clearResult();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(AppLocalization.of(context).errorWhileSharing),
action: SnackBarAction(
label: AppLocalization.of(context).tryAgain,
onPressed: widget.viewModel.shareBooking.execute,
),
));
}
}
}

View File

@@ -0,0 +1,121 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
/// Simple Localizations similar to
/// https://docs.flutter.dev/ui/accessibility-and-internationalization/internationalization#an-alternative-class-for-the-apps-localized-resources
class AppLocalization {
static AppLocalization of(BuildContext context) {
return Localizations.of(context, AppLocalization);
}
static const _strings = <String, String>{
'activities': 'Activities',
'addDates': 'Add Dates',
'bookingDeleted': 'Booking deleted',
'bookNewTrip': 'Book New Trip',
'close': 'Close',
'confirm': 'Confirm',
'daytime': 'Daytime',
'errorWhileDeletingBooking': 'Error while deleting booking',
'errorWhileLoadingActivities': 'Error while loading activities',
'errorWhileLoadingBooking': 'Error while loading booking',
'errorWhileLoadingContinents': 'Error while loading continents',
'errorWhileLoadingDestinations': 'Error while loading destinations',
'errorWhileLoadingHome': 'Error while loading home',
'errorWhileLogin': 'Error while trying to login',
'errorWhileLogout': 'Error while trying to logout',
'errorWhileSavingActivities': 'Error while saving activities',
'errorWhileSavingItinerary': 'Error while saving itinerary',
'errorWhileSharing': 'Error while sharing booking',
'evening': 'Evening',
'login': 'Login',
'nameTrips': '{name}\'s Trips',
'search': 'Search',
'searchDestination': 'Search destination',
'selected': '{1} selected',
'shareTrip': 'Share Trip',
'tryAgain': 'Try again',
'yourChosenActivities': 'Your chosen activities',
'when': 'When',
};
// If string for "label" does not exist, will show "[LABEL]"
static String _get(String label) =>
_strings[label] ?? '[${label.toUpperCase()}]';
String get activities => _get('activities');
String get addDates => _get('addDates');
String get confirm => _get('confirm');
String get daytime => _get('daytime');
String get errorWhileLoadingActivities => _get('errorWhileLoadingActivities');
String get errorWhileLoadingBooking => _get('errorWhileLoadingBooking');
String get errorWhileLoadingContinents => _get('errorWhileLoadingContinents');
String get errorWhileLoadingDestinations =>
_get('errorWhileLoadingDestinations');
String get errorWhileSavingActivities => _get('errorWhileSavingActivities');
String get errorWhileSavingItinerary => _get('errorWhileSavingItinerary');
String get evening => _get('evening');
String get search => _get('search');
String get searchDestination => _get('searchDestination');
String get shareTrip => _get('shareTrip');
String get tryAgain => _get('tryAgain');
String get yourChosenActivities => _get('yourChosenActivities');
String get when => _get('when');
String get errorWhileLogin => _get('errorWhileLogin');
String get login => _get('login');
String get errorWhileLogout => _get('errorWhileLogout');
String get close => _get('close');
String get errorWhileSharing => _get('errorWhileSharing');
String get bookNewTrip => _get('bookNewTrip');
String get errorWhileLoadingHome => _get('errorWhileLoadingHome');
String get bookingDeleted => _get('bookingDeleted');
String get errorWhileDeletingBooking => _get('errorWhileDeletingBooking');
String nameTrips(String name) => _get('nameTrips').replaceAll('{name}', name);
String selected(int value) =>
_get('selected').replaceAll('{1}', value.toString());
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalization> {
@override
bool isSupported(Locale locale) => locale.languageCode == 'en';
@override
Future<AppLocalization> load(Locale locale) {
return SynchronousFuture(AppLocalization());
}
@override
bool shouldReload(covariant LocalizationsDelegate<AppLocalization> old) =>
false;
}

View File

@@ -0,0 +1,41 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
class AppColors {
static const black1 = Color(0xFF101010);
static const white1 = Color(0xFFFFF7FA);
static const grey1 = Color(0xFFF2F2F2);
static const grey2 = Color(0xFF4D4D4D);
static const grey3 = Color(0xFFA4A4A4);
static const whiteTransparent =
Color(0x4DFFFFFF); // Figma rgba(255, 255, 255, 0.3)
static const blackTransparent = Color(0x4D000000);
static const red1 = Color(0xFFE74C3C);
static const lightColorScheme = ColorScheme(
brightness: Brightness.light,
primary: AppColors.black1,
onPrimary: AppColors.white1,
secondary: AppColors.black1,
onSecondary: AppColors.white1,
surface: Colors.white,
onSurface: AppColors.black1,
error: Colors.white,
onError: Colors.red,
);
static const darkColorScheme = ColorScheme(
brightness: Brightness.dark,
primary: AppColors.white1,
onPrimary: AppColors.black1,
secondary: AppColors.white1,
onSecondary: AppColors.black1,
surface: AppColors.black1,
onSurface: Colors.white,
error: Colors.black,
onError: AppColors.red1,
);
}

View File

@@ -0,0 +1,65 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
sealed class Dimens {
const Dimens();
/// General horizontal padding used to separate UI items
static const paddingHorizontal = 20.0;
/// General vertical padding used to separate UI items
static const paddingVertical = 24.0;
/// Horizontal padding for screen edges
abstract final double paddingScreenHorizontal;
/// Vertical padding for screen edges
abstract final double paddingScreenVertical;
/// Horizontal symmetric padding for screen edges
EdgeInsets get edgeInsetsScreenHorizontal =>
EdgeInsets.symmetric(horizontal: paddingScreenHorizontal);
/// Symmetric padding for screen edges
EdgeInsets get edgeInsetsScreenSymmetric => EdgeInsets.symmetric(
horizontal: paddingScreenHorizontal, vertical: paddingScreenVertical);
static final dimensDesktop = DimensDesktop();
static final dimensMobile = DimensMobile();
/// Get dimensions definition based on screen size
factory Dimens.of(BuildContext context) =>
switch (MediaQuery.sizeOf(context).width) {
> 600 => dimensDesktop,
_ => dimensMobile,
};
abstract final double profilePictureSize;
}
/// Mobile dimensions
class DimensMobile extends Dimens {
@override
double paddingScreenHorizontal = Dimens.paddingHorizontal;
@override
double paddingScreenVertical = Dimens.paddingVertical;
@override
double get profilePictureSize => 64.0;
}
/// Desktop/Web dimensions
class DimensDesktop extends Dimens {
@override
double paddingScreenHorizontal = 100.0;
@override
double paddingScreenVertical = 64.0;
@override
double get profilePictureSize => 128.0;
}

View File

@@ -0,0 +1,84 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'colors.dart';
import '../ui/tag_chip.dart';
import 'package:flutter/material.dart';
class AppTheme {
static const _textTheme = TextTheme(
headlineLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.w500,
),
headlineSmall: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w400,
),
titleMedium: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
),
bodyLarge: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w400,
),
bodyMedium: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w400,
),
bodySmall: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: AppColors.grey3,
),
labelSmall: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w500,
color: AppColors.grey3,
),
labelLarge: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w400,
color: AppColors.grey3,
),
);
static const _inputDecorationTheme = InputDecorationTheme(
hintStyle: TextStyle(
// grey3 works for both light and dark themes
color: AppColors.grey3,
fontSize: 18.0,
fontWeight: FontWeight.w400,
),
);
static ThemeData lightTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: AppColors.lightColorScheme,
textTheme: _textTheme,
inputDecorationTheme: _inputDecorationTheme,
extensions: [
TagChipTheme(
chipColor: AppColors.whiteTransparent,
onChipColor: Colors.white,
),
],
);
static ThemeData darkTheme = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: AppColors.darkColorScheme,
textTheme: _textTheme,
inputDecorationTheme: _inputDecorationTheme,
extensions: [
TagChipTheme(
chipColor: AppColors.blackTransparent,
onChipColor: Colors.white,
),
],
);
}

View File

@@ -0,0 +1,63 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../themes/colors.dart';
import 'blur_filter.dart';
/// Custom back button to pop navigation.
class CustomBackButton extends StatelessWidget {
const CustomBackButton({
super.key,
this.onTap,
this.blur = false,
});
final bool blur;
final GestureTapCallback? onTap;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40.0,
width: 40.0,
child: Stack(
children: [
if (blur)
ClipRect(
child: BackdropFilter(
filter: kBlurFilter,
child: const SizedBox(height: 40.0, width: 40.0),
),
),
DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey1),
borderRadius: BorderRadius.circular(8.0),
),
child: InkWell(
borderRadius: BorderRadius.circular(8.0),
onTap: () {
if (onTap != null) {
onTap!();
} else {
context.pop();
}
},
child: Center(
child: Icon(
size: 24.0,
Icons.arrow_back,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,7 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
final kBlurFilter = ImageFilter.blur(sigmaX: 2, sigmaY: 2);

View File

@@ -0,0 +1,50 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../themes/colors.dart';
class CustomCheckbox extends StatelessWidget {
const CustomCheckbox({
super.key,
required this.value,
required this.onChanged,
});
final bool value;
final ValueChanged<bool?> onChanged;
@override
Widget build(BuildContext context) {
return InkResponse(
radius: 24,
onTap: () => onChanged(!value),
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24),
border: Border.all(color: AppColors.grey3),
),
child: Material(
borderRadius: BorderRadius.circular(24),
color: value
? Theme.of(context).colorScheme.primary
: Colors.transparent,
child: SizedBox(
width: 24,
height: 24,
child: Visibility(
visible: value,
child: Icon(
Icons.check,
size: 14,
color: Theme.of(context).colorScheme.onPrimary,
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,24 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
final _dateFormatDay = DateFormat('d');
final _dateFormatDayMonth = DateFormat('d MMM');
String dateFormatStartEnd(DateTimeRange dateTimeRange) {
final start = dateTimeRange.start;
final end = dateTimeRange.end;
final dayMonthEnd = _dateFormatDayMonth.format(end);
if (start.month == end.month) {
final dayStart = _dateFormatDay.format(start);
return '$dayStart - $dayMonthEnd';
}
final dayMonthStart = _dateFormatDayMonth.format(start);
return '$dayMonthStart - $dayMonthEnd';
}

View File

@@ -0,0 +1,63 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../themes/colors.dart';
class ErrorIndicator extends StatelessWidget {
const ErrorIndicator({
super.key,
required this.title,
required this.label,
required this.onPressed,
});
final String title;
final String label;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IntrinsicWidth(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Row(
children: [
Icon(
Icons.error_outline,
color: Theme.of(context).colorScheme.onError,
),
const SizedBox(width: 10),
Text(
title,
style: TextStyle(
color: Theme.of(context).colorScheme.onError,
),
),
],
),
),
),
),
const SizedBox(
height: 10,
),
FilledButton(
onPressed: onPressed,
style: const ButtonStyle(
backgroundColor: WidgetStatePropertyAll(AppColors.red1),
foregroundColor: WidgetStatePropertyAll(Colors.white),
),
child: Text(label),
),
],
);
}
}

View File

@@ -0,0 +1,60 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../routing/routes.dart';
import '../themes/colors.dart';
import 'blur_filter.dart';
/// Home button to navigate back to the '/' path.
class HomeButton extends StatelessWidget {
const HomeButton({
super.key,
this.blur = false,
});
final bool blur;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40.0,
width: 40.0,
child: Stack(
fit: StackFit.expand,
children: [
if (blur)
ClipRect(
child: BackdropFilter(
filter: kBlurFilter,
child: const SizedBox(height: 40.0, width: 40.0),
),
),
DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey1),
borderRadius: BorderRadius.circular(8.0),
color: Colors.transparent,
),
child: InkWell(
borderRadius: BorderRadius.circular(8.0),
onTap: () {
context.go(Routes.home);
},
child: Center(
child: Icon(
size: 24.0,
Icons.home_outlined,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,17 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
/// Custom scroll behavior to allow dragging with mouse.
/// Necessary to allow dragging with mouse on Continents carousel.
class AppCustomScrollBehavior extends MaterialScrollBehavior {
@override
Set<PointerDeviceKind> get dragDevices => {
PointerDeviceKind.touch,
// Allow to drag with mouse on Regions carousel
PointerDeviceKind.mouse,
};
}

View File

@@ -0,0 +1,111 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../../domain/models/itinerary_config/itinerary_config.dart';
import '../localization/applocalization.dart';
import '../themes/colors.dart';
import '../themes/dimens.dart';
import 'date_format_start_end.dart';
import 'home_button.dart';
/// Application top search bar.
///
/// Displays a search bar with the current configuration.
/// Includes [HomeButton] to navigate back to the '/' path.
class AppSearchBar extends StatelessWidget {
const AppSearchBar({
super.key,
this.config,
this.onTap,
});
final ItineraryConfig? config;
final GestureTapCallback? onTap;
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: InkWell(
borderRadius: BorderRadius.circular(16.0),
onTap: onTap,
child: Container(
height: 64,
decoration: BoxDecoration(
border: Border.all(color: AppColors.grey1),
borderRadius: BorderRadius.circular(16.0),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Dimens.paddingHorizontal,
),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: _QueryText(config: config),
),
),
),
),
),
const SizedBox(width: 10),
const HomeButton(),
],
);
}
}
class _QueryText extends StatelessWidget {
const _QueryText({
required this.config,
});
final ItineraryConfig? config;
@override
Widget build(BuildContext context) {
if (config == null) {
return const _EmptySearch();
}
final ItineraryConfig(:continent, :startDate, :endDate, :guests) = config!;
if (startDate == null ||
endDate == null ||
guests == null ||
continent == null) {
return const _EmptySearch();
}
return Text(
'$continent - ${dateFormatStartEnd(DateTimeRange(start: startDate, end: endDate))} - Guests: $guests',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge,
);
}
}
class _EmptySearch extends StatelessWidget {
const _EmptySearch();
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Icon(Icons.search),
const SizedBox(width: 12),
Expanded(
child: Text(
AppLocalization.of(context).searchDestination,
textAlign: TextAlign.start,
style: Theme.of(context).inputDecorationTheme.hintStyle,
),
),
],
);
}
}

View File

@@ -0,0 +1,144 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import '../themes/colors.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
class TagChip extends StatelessWidget {
const TagChip({
super.key,
required this.tag,
this.fontSize = 10,
this.height = 20,
this.chipColor,
this.onChipColor,
});
final String tag;
final double fontSize;
final double height;
final Color? chipColor;
final Color? onChipColor;
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(height / 2),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3),
child: DecoratedBox(
decoration: BoxDecoration(
color: chipColor ??
Theme.of(context).extension<TagChipTheme>()?.chipColor ??
AppColors.whiteTransparent,
),
child: SizedBox(
height: height,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_iconFrom(tag),
color: onChipColor ??
Theme.of(context)
.extension<TagChipTheme>()
?.onChipColor ??
Colors.white,
size: fontSize,
),
const SizedBox(width: 4),
Text(
tag,
textAlign: TextAlign.center,
style: _textStyle(context),
),
],
),
),
),
),
),
);
}
IconData? _iconFrom(String tag) {
return switch (tag) {
'Adventure sports' => Icons.kayaking_outlined,
'Beach' => Icons.beach_access_outlined,
'City' => Icons.location_city_outlined,
'Cultural experiences' => Icons.museum_outlined,
'Foodie' || 'Food tours' => Icons.restaurant,
'Hiking' => Icons.hiking,
'Historic' => Icons.menu_book_outlined,
'Island' || 'Coastal' || 'Lake' || 'River' => Icons.water,
'Luxury' => Icons.attach_money_outlined,
'Mountain' || 'Wildlife watching' => Icons.landscape_outlined,
'Nightlife' => Icons.local_bar_outlined,
'Off-the-beaten-path' => Icons.do_not_step_outlined,
'Romantic' => Icons.favorite_border_outlined,
'Rural' => Icons.agriculture_outlined,
'Secluded' => Icons.church_outlined,
'Sightseeing' => Icons.attractions_outlined,
'Skiing' => Icons.downhill_skiing_outlined,
'Wine tasting' => Icons.wine_bar_outlined,
'Winter destination' => Icons.ac_unit,
_ => Icons.label_outlined,
};
}
// Note: original Figma file uses Google Sans
// which is not available on GoogleFonts
_textStyle(BuildContext context) => GoogleFonts.openSans(
textStyle: TextStyle(
fontWeight: FontWeight.w500,
fontSize: fontSize,
color: onChipColor ??
Theme.of(context).extension<TagChipTheme>()?.onChipColor ??
Colors.white,
textBaseline: TextBaseline.alphabetic,
),
);
}
class TagChipTheme extends ThemeExtension<TagChipTheme> {
final Color chipColor;
final Color onChipColor;
TagChipTheme({
required this.chipColor,
required this.onChipColor,
});
@override
ThemeExtension<TagChipTheme> copyWith({
Color? chipColor,
Color? onChipColor,
}) {
return TagChipTheme(
chipColor: chipColor ?? this.chipColor,
onChipColor: onChipColor ?? this.onChipColor,
);
}
@override
ThemeExtension<TagChipTheme> lerp(
covariant ThemeExtension<TagChipTheme> other,
double t,
) {
if (other is! TagChipTheme) {
return this;
}
return TagChipTheme(
chipColor: Color.lerp(chipColor, other.chipColor, t) ?? chipColor,
onChipColor: Color.lerp(onChipColor, other.onChipColor, t) ?? onChipColor,
);
}
}

View File

@@ -0,0 +1,95 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import '../../../data/repositories/booking/booking_repository.dart';
import '../../../data/repositories/user/user_repository.dart';
import '../../../domain/models/booking/booking_summary.dart';
import '../../../domain/models/user/user.dart';
import '../../../utils/command.dart';
import '../../../utils/result.dart';
class HomeViewModel extends ChangeNotifier {
HomeViewModel({
required BookingRepository bookingRepository,
required UserRepository userRepository,
}) : _bookingRepository = bookingRepository,
_userRepository = userRepository {
load = Command0(_load)..execute();
deleteBooking = Command1(_deleteBooking);
}
final BookingRepository _bookingRepository;
final UserRepository _userRepository;
final _log = Logger('HomeViewModel');
List<BookingSummary> _bookings = [];
User? _user;
late Command0 load;
late Command1<void, int> deleteBooking;
List<BookingSummary> get bookings => _bookings;
User? get user => _user;
Future<Result> _load() async {
try {
final result = await _bookingRepository.getBookingsList();
switch (result) {
case Ok<List<BookingSummary>>():
_bookings = result.value;
_log.fine('Loaded bookings');
case Error<List<BookingSummary>>():
_log.warning('Failed to load bookings', result.error);
return result;
}
final userResult = await _userRepository.getUser();
switch (userResult) {
case Ok<User>():
_user = userResult.value;
_log.fine('Loaded user');
case Error<User>():
_log.warning('Failed to load user', userResult.error);
}
return userResult;
} finally {
notifyListeners();
}
}
Future<Result<void>> _deleteBooking(int id) async {
try {
final resultDelete = await _bookingRepository.delete(id);
switch (resultDelete) {
case Ok<void>():
_log.fine('Deleted booking $id');
case Error<void>():
_log.warning('Failed to delete booking $id', resultDelete.error);
return resultDelete;
}
// After deleting the booking, we need to reload the bookings list.
// BookingRepository is the source of truth for bookings.
final resultLoadBookings = await _bookingRepository.getBookingsList();
switch (resultLoadBookings) {
case Ok<List<BookingSummary>>():
_bookings = resultLoadBookings.value;
_log.fine('Loaded bookings');
case Error<List<BookingSummary>>():
_log.warning('Failed to load bookings', resultLoadBookings.error);
return resultLoadBookings;
}
return resultLoadBookings;
} finally {
notifyListeners();
}
}
}

View File

@@ -0,0 +1,198 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../../domain/models/booking/booking_summary.dart';
import '../../../routing/routes.dart';
import '../../core/localization/applocalization.dart';
import '../../core/themes/dimens.dart';
import '../../core/ui/date_format_start_end.dart';
import '../../core/ui/error_indicator.dart';
import '../view_models/home_viewmodel.dart';
import 'home_title.dart';
const String bookingButtonKey = 'booking-button';
class HomeScreen extends StatefulWidget {
const HomeScreen({
super.key,
required this.viewModel,
});
final HomeViewModel viewModel;
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
void initState() {
super.initState();
widget.viewModel.deleteBooking.addListener(_onResult);
}
@override
void didUpdateWidget(covariant HomeScreen oldWidget) {
super.didUpdateWidget(oldWidget);
oldWidget.viewModel.deleteBooking.removeListener(_onResult);
widget.viewModel.deleteBooking.addListener(_onResult);
}
@override
void dispose() {
widget.viewModel.deleteBooking.removeListener(_onResult);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton.extended(
// Workaround for https://github.com/flutter/flutter/issues/115358#issuecomment-2117157419
heroTag: null,
key: const ValueKey(bookingButtonKey),
onPressed: () => context.go(Routes.search),
label: Text(AppLocalization.of(context).bookNewTrip),
icon: const Icon(Icons.add_location_outlined),
),
body: SafeArea(
top: true,
bottom: true,
child: ListenableBuilder(
listenable: widget.viewModel.load,
builder: (context, child) {
if (widget.viewModel.load.running) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (widget.viewModel.load.error) {
return ErrorIndicator(
title: AppLocalization.of(context).errorWhileLoadingHome,
label: AppLocalization.of(context).tryAgain,
onPressed: widget.viewModel.load.execute,
);
}
return child!;
},
child: ListenableBuilder(
listenable: widget.viewModel,
builder: (context, _) {
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.symmetric(
vertical: Dimens.of(context).paddingScreenVertical,
horizontal: Dimens.of(context).paddingScreenHorizontal,
),
child: HomeHeader(viewModel: widget.viewModel),
),
),
SliverList.builder(
itemCount: widget.viewModel.bookings.length,
itemBuilder: (_, index) => _Booking(
key: ValueKey(widget.viewModel.bookings[index].id),
booking: widget.viewModel.bookings[index],
onTap: () => context.push(Routes.bookingWithId(
widget.viewModel.bookings[index].id)),
confirmDismiss: (_) async {
// wait for command to complete
await widget.viewModel.deleteBooking.execute(
widget.viewModel.bookings[index].id,
);
// if command completed successfully, return true
if (widget.viewModel.deleteBooking.completed) {
// removes the dismissable from the list
return true;
} else {
// the dismissable stays in the list
return false;
}
},
),
)
],
);
},
),
),
),
);
}
void _onResult() {
if (widget.viewModel.deleteBooking.completed) {
widget.viewModel.deleteBooking.clearResult();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalization.of(context).bookingDeleted),
),
);
}
if (widget.viewModel.deleteBooking.error) {
widget.viewModel.deleteBooking.clearResult();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(AppLocalization.of(context).errorWhileDeletingBooking),
),
);
}
}
}
class _Booking extends StatelessWidget {
const _Booking({
super.key,
required this.booking,
required this.onTap,
required this.confirmDismiss,
});
final BookingSummary booking;
final GestureTapCallback onTap;
final ConfirmDismissCallback confirmDismiss;
@override
Widget build(BuildContext context) {
return Dismissible(
key: ValueKey(booking.id),
direction: DismissDirection.endToStart,
confirmDismiss: confirmDismiss,
child: InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: Dimens.of(context).paddingScreenHorizontal,
vertical: Dimens.paddingVertical,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
booking.name,
style: Theme.of(context).textTheme.titleLarge,
),
Text(
dateFormatStartEnd(
DateTimeRange(
start: booking.startDate,
end: booking.endDate,
),
),
style: Theme.of(context).textTheme.bodyLarge,
),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,89 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import '../../auth/logout/view_models/logout_viewmodel.dart';
import '../../auth/logout/widgets/logout_button.dart';
import '../../core/localization/applocalization.dart';
import '../../core/themes/dimens.dart';
import '../view_models/home_viewmodel.dart';
class HomeHeader extends StatelessWidget {
const HomeHeader({
super.key,
required this.viewModel,
});
final HomeViewModel viewModel;
@override
Widget build(BuildContext context) {
final user = viewModel.user;
if (user == null) {
return const SizedBox();
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipOval(
child: Image.asset(
user.picture,
width: Dimens.of(context).profilePictureSize,
height: Dimens.of(context).profilePictureSize,
),
),
LogoutButton(
viewModel: LogoutViewModel(
authRepository: context.read(),
itineraryConfigRepository: context.read(),
),
),
],
),
const SizedBox(height: Dimens.paddingVertical),
_Title(
text: AppLocalization.of(context).nameTrips(user.name),
),
],
);
}
}
class _Title extends StatelessWidget {
const _Title({
required this.text,
});
final String text;
@override
Widget build(BuildContext context) {
return ShaderMask(
blendMode: BlendMode.srcIn,
shaderCallback: (bounds) => RadialGradient(
center: Alignment.bottomLeft,
radius: 2,
colors: [
Colors.purple.shade700,
Colors.purple.shade400,
],
).createShader(
Rect.fromLTWH(0, 0, bounds.width, bounds.height),
),
child: Text(
text,
style: GoogleFonts.rubik(
textStyle: Theme.of(context).textTheme.headlineLarge,
),
),
);
}
}

View File

@@ -0,0 +1,111 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:logging/logging.dart';
import '../../../data/repositories/destination/destination_repository.dart';
import '../../../data/repositories/itinerary_config/itinerary_config_repository.dart';
import '../../../domain/models/destination/destination.dart';
import '../../../domain/models/itinerary_config/itinerary_config.dart';
import '../../../utils/command.dart';
import '../../../utils/result.dart';
import 'package:flutter/cupertino.dart';
/// Results screen view model
/// Based on https://docs.flutter.dev/get-started/fwe/state-management#using-mvvm-for-your-applications-architecture
class ResultsViewModel extends ChangeNotifier {
ResultsViewModel({
required DestinationRepository destinationRepository,
required ItineraryConfigRepository itineraryConfigRepository,
}) : _destinationRepository = destinationRepository,
_itineraryConfigRepository = itineraryConfigRepository {
updateItineraryConfig = Command1<void, String>(_updateItineraryConfig);
search = Command0(_search)..execute();
}
final _log = Logger('ResultsViewModel');
final DestinationRepository _destinationRepository;
final ItineraryConfigRepository _itineraryConfigRepository;
// Setters are private
List<Destination> _destinations = [];
/// List of destinations, may be empty but never null
List<Destination> get destinations => _destinations;
ItineraryConfig? _itineraryConfig;
/// Filter options to display on search bar
ItineraryConfig get config => _itineraryConfig ?? const ItineraryConfig();
/// Perform search
late final Command0 search;
/// Store ViewModel data into [ItineraryConfigRepository] before navigating.
late final Command1<void, String> updateItineraryConfig;
Future<Result<void>> _search() async {
// Load current itinerary config
final resultConfig = await _itineraryConfigRepository.getItineraryConfig();
if (resultConfig is Error) {
_log.warning(
'Failed to load stored ItineraryConfig',
resultConfig.asError.error,
);
return resultConfig;
}
_itineraryConfig = resultConfig.asOk.value;
notifyListeners();
final result = await _destinationRepository.getDestinations();
switch (result) {
case Ok():
{
// If the result is Ok, update the list of destinations
_destinations = result.value
.where((destination) =>
destination.continent == _itineraryConfig!.continent)
.toList();
_log.fine('Destinations (${_destinations.length}) loaded');
}
case Error():
{
_log.warning('Failed to load destinations', result.error);
}
}
// After finish loading results, notify the view
notifyListeners();
return result;
}
Future<Result<void>> _updateItineraryConfig(String destinationRef) async {
assert(destinationRef.isNotEmpty, "destinationRef should not be empty");
final resultConfig = await _itineraryConfigRepository.getItineraryConfig();
if (resultConfig is Error) {
_log.warning(
'Failed to load stored ItineraryConfig',
resultConfig.asError.error,
);
return resultConfig;
}
final itineraryConfig = resultConfig.asOk.value;
final result = await _itineraryConfigRepository
.setItineraryConfig(itineraryConfig.copyWith(
destination: destinationRef,
activities: [],
));
if (result is Error) {
_log.warning(
'Failed to store ItineraryConfig',
result.asError.error,
);
}
return result;
}
}

View File

@@ -0,0 +1,89 @@
// Copyright 2024 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../domain/models/destination/destination.dart';
import '../../../utils/image_error_listener.dart';
import '../../core/ui/tag_chip.dart';
class ResultCard extends StatelessWidget {
const ResultCard({
super.key,
required this.destination,
required this.onTap,
});
final Destination destination;
final GestureTapCallback onTap;
@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(10.0),
child: Stack(
fit: StackFit.expand,
children: [
CachedNetworkImage(
imageUrl: destination.imageUrl,
fit: BoxFit.fitHeight,
errorWidget: (context, url, error) => const Icon(Icons.error),
errorListener: imageErrorListener,
),
Positioned(
bottom: 12.0,
left: 12.0,
right: 12.0,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
destination.name.toUpperCase(),
style: _cardTitleStyle,
),
const SizedBox(
height: 6,
),
Wrap(
spacing: 4.0,
runSpacing: 4.0,
direction: Axis.horizontal,
children:
destination.tags.map((e) => TagChip(tag: e)).toList(),
),
],
),
),
// Handle taps
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
),
),
),
],
),
);
}
}
final _cardTitleStyle = GoogleFonts.rubik(
textStyle: const TextStyle(
fontWeight: FontWeight.w800,
fontSize: 15.0,
color: Colors.white,
letterSpacing: 1,
shadows: [
// Helps to read the text a bit better
Shadow(
blurRadius: 3.0,
color: Colors.black,
)
],
),
);

Some files were not shown because too many files have changed in this diff Show More