From 8361b0c62c116cf2051764151becba0170333018 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Sun, 15 Sep 2024 17:34:48 -0400 Subject: [PATCH 01/34] feat: add environment configuration for API KEY --- .env.example | 1 + .fvm/fvm_config.json | 3 +-- .gitignore | 8 +++++++- .vscode/settings.json | 14 +++++++------- lib/main.dart | 10 +++++++--- pubspec.lock | 8 ++++++++ pubspec.yaml | 3 +++ 7 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d97480f --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +YEP_API_KEY= \ No newline at end of file diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index 160b5b2..e7b55eb 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,3 @@ { - "flutterSdkVersion": "3.22.3", - "flavors": {} + "flutterSdkVersion": "3.22.3" } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1be2d87..1a391ad 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,10 @@ app.*.map.json /android/app/release # fvm -.fvm/flutter_sdk \ No newline at end of file + +# FVM Version Cache +.fvm/ +.fvmrc + +# ENV +.env \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f285aa4..0e6ea20 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { - "dart.flutterSdkPath": ".fvm/flutter_sdk", - "search.exclude": { - "**/.fvm": true - }, - "files.watcherExclude": { - "**/.fvm": true - } + "dart.flutterSdkPath": ".fvm/flutter_sdk", + "search.exclude": { + "**/.fvm": true + }, + "files.watcherExclude": { + "**/.fvm": true + } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index ae7012a..b5077d4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:http/http.dart' as http; import 'package:restaurant_tour/models/restaurant.dart'; import 'package:restaurant_tour/query.dart'; @@ -8,8 +9,11 @@ import 'package:restaurant_tour/query.dart'; const _apiKey = ''; const _baseUrl = 'https://api.yelp.com/v3/graphql'; -void main() { - runApp(const RestaurantTour()); +Future main() async { + await dotenv.load(fileName: ".env"); + runApp( + const RestaurantTour(), + ); } class RestaurantTour extends StatelessWidget { @@ -31,7 +35,7 @@ class HomePage extends StatelessWidget { Future getRestaurants({int offset = 0}) async { final headers = { - 'Authorization': 'Bearer $_apiKey', + 'Authorization': 'Bearer ${dotenv.env['YEP_API_KEY']}', 'Content-Type': 'application/graphql', }; diff --git a/pubspec.lock b/pubspec.lock index f95a63e..fc04df1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -206,6 +206,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" flutter_lints: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index bc8a205..c9b5d82 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,7 @@ environment: dependencies: flutter: sdk: flutter + flutter_dotenv: ^5.1.0 http: ^1.2.2 json_annotation: ^4.9.0 @@ -26,6 +27,8 @@ dev_dependencies: flutter: generate: true uses-material-design: true + assets: + - .env fonts: - family: Lora fonts: From c91bdac226235871b7fb38ab5e27984a9dfc6135 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 01:27:44 -0400 Subject: [PATCH 02/34] refactor: service yelp, folders restructured to clean --- lib/{ => core}/models/restaurant.dart | 0 lib/{ => core}/models/restaurant.g.dart | 0 .../presenter/screen/home_screen.dart | 37 ++++++ lib/main.dart | 115 +++++++++--------- lib/repositories/yelp_repository.dart | 68 +++++++++++ pubspec.lock | 16 +++ pubspec.yaml | 1 + 7 files changed, 180 insertions(+), 57 deletions(-) rename lib/{ => core}/models/restaurant.dart (100%) rename lib/{ => core}/models/restaurant.g.dart (100%) create mode 100644 lib/features/home_page/presenter/screen/home_screen.dart create mode 100644 lib/repositories/yelp_repository.dart diff --git a/lib/models/restaurant.dart b/lib/core/models/restaurant.dart similarity index 100% rename from lib/models/restaurant.dart rename to lib/core/models/restaurant.dart diff --git a/lib/models/restaurant.g.dart b/lib/core/models/restaurant.g.dart similarity index 100% rename from lib/models/restaurant.g.dart rename to lib/core/models/restaurant.g.dart diff --git a/lib/features/home_page/presenter/screen/home_screen.dart b/lib/features/home_page/presenter/screen/home_screen.dart new file mode 100644 index 0000000..477473c --- /dev/null +++ b/lib/features/home_page/presenter/screen/home_screen.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:restaurant_tour/repositories/yelp_repository.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Restaurant Tour'), + ElevatedButton( + onPressed: () async { + final yelpRepo = YelpRepository(); + try { + final result = await yelpRepo.getRestaurants(); + if(result != null) { + debugPrint('Fetched ${result.restaurants!.length} restaurants'); + } else { + debugPrint('No restaurants fetched'); + } + } catch (e) { + debugPrint('Failed to fetch restaurants: $e'); + } + }, + child: const Text('Fetch Restaurants'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index b5077d4..c282693 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,7 +3,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:http/http.dart' as http; -import 'package:restaurant_tour/models/restaurant.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_page/presenter/screen/home_screen.dart'; import 'package:restaurant_tour/query.dart'; const _apiKey = ''; @@ -23,69 +24,69 @@ class RestaurantTour extends StatelessWidget { Widget build(BuildContext context) { return const MaterialApp( title: 'Restaurant Tour', - home: HomePage(), + home: HomeScreen(), ); } } // TODO: Architect code // This is just a POC of the API integration -class HomePage extends StatelessWidget { - const HomePage({super.key}); +// class HomePage extends StatelessWidget { +// const HomePage({super.key}); - Future getRestaurants({int offset = 0}) async { - final headers = { - 'Authorization': 'Bearer ${dotenv.env['YEP_API_KEY']}', - 'Content-Type': 'application/graphql', - }; +// Future getRestaurants({int offset = 0}) async { +// final headers = { +// 'Authorization': 'Bearer ${dotenv.env['YEP_API_KEY']}', +// 'Content-Type': 'application/graphql', +// }; - try { - final response = await http.post( - Uri.parse(_baseUrl), - headers: headers, - body: query(offset), - ); +// try { +// final response = await http.post( +// Uri.parse(_baseUrl), +// headers: headers, +// body: query(offset), +// ); - if (response.statusCode == 200) { - return RestaurantQueryResult.fromJson( - jsonDecode(response.body)['data']['search'], - ); - } else { - print('Failed to load restaurants: ${response.statusCode}'); - return null; - } - } catch (e) { - print('Error fetching restaurants: $e'); - return null; - } - } +// if (response.statusCode == 200) { +// return RestaurantQueryResult.fromJson( +// jsonDecode(response.body)['data']['search'], +// ); +// } else { +// print('Failed to load restaurants: ${response.statusCode}'); +// return null; +// } +// } catch (e) { +// print('Error fetching restaurants: $e'); +// return null; +// } +// } - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Restaurant Tour'), - ElevatedButton( - child: const Text('Fetch Restaurants'), - onPressed: () async { - try { - final result = await getRestaurants(); - if (result != null) { - print('Fetched ${result.restaurants!.length} restaurants'); - } else { - print('No restaurants fetched'); - } - } catch (e) { - print('Failed to fetch restaurants: $e'); - } - }, - ), - ], - ), - ), - ); - } -} +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// body: Center( +// child: Column( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// const Text('Restaurant Tour'), +// ElevatedButton( +// child: const Text('Fetch Restaurants'), +// onPressed: () async { +// try { +// final result = await getRestaurants(); +// if (result != null) { +// print('Fetched ${result.restaurants!.length} restaurants'); +// } else { +// print('No restaurants fetched'); +// } +// } catch (e) { +// print('Failed to fetch restaurants: $e'); +// } +// }, +// ), +// ], +// ), +// ), +// ); +// } +// } diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart new file mode 100644 index 0000000..1fe489f --- /dev/null +++ b/lib/repositories/yelp_repository.dart @@ -0,0 +1,68 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; + +class YelpRepository { + late Dio dio; + + YelpRepository() { + dio = Dio( + BaseOptions( + baseUrl: 'https://api.yelp.com', + headers: { + 'Authorization': 'Bearer ${dotenv.env['YELP_API_KEY']}', + 'Content-Type': 'application/graphql', + }, + ), + ); + } + + Future getRestaurants({int offset = 0}) async { + try { + final response = await dio.post>( + '/v3/graphql', + data: _getQuery(offset), + ); + return RestaurantQueryResult.fromJson(response.data!['data']['search']); + } catch (e) { + print('Error fetching restaurants: $e'); + return null; + } + } + + String _getQuery(int offset) { + return ''' + query getRestaurants { + search(location: "Las Vegas", limit: 20, offset: $offset) { + total + business { + id + name + price + rating + photos + reviews { + id + rating + user { + id + image_url + name + } + } + categories { + title + alias + } + hours { + is_open_now + } + location { + formatted_address + } + } + } + } + '''; + } +} diff --git a/pubspec.lock b/pubspec.lock index fc04df1..a234e73 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -177,6 +177,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + dio: + dependency: "direct main" + description: + name: dio + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + url: "https://pub.dev" + source: hosted + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + url: "https://pub.dev" + source: hosted + version: "2.0.0" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c9b5d82..aa9f207 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,7 @@ environment: flutter: ">=3.19.6" dependencies: + dio: ^5.7.0 flutter: sdk: flutter flutter_dotenv: ^5.1.0 From de3131c0f703c963ad804e25cfe573351863d02e Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 02:29:42 -0400 Subject: [PATCH 03/34] feat: splash screen, flutter bloc, router navigation --- assets/images/restaurantour-logo.png | Bin 0 -> 242977 bytes lib/core/navigation/route_navigator.dart | 22 +++++ .../presenter/screen/home_screen.dart | 1 - .../presenter/bloc/splash_screen_bloc.dart | 19 +++++ .../presenter/bloc/splash_screen_event.dart | 12 +++ .../presenter/bloc/splash_screen_state.dart | 17 ++++ .../presenter/splash_screen.dart | 49 +++++++++++ lib/main.dart | 79 ++---------------- pubspec.lock | 61 ++++++++++++++ pubspec.yaml | 5 ++ 10 files changed, 190 insertions(+), 75 deletions(-) create mode 100644 assets/images/restaurantour-logo.png create mode 100644 lib/core/navigation/route_navigator.dart create mode 100644 lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart create mode 100644 lib/features/splash_screen/presenter/bloc/splash_screen_event.dart create mode 100644 lib/features/splash_screen/presenter/bloc/splash_screen_state.dart create mode 100644 lib/features/splash_screen/presenter/splash_screen.dart diff --git a/assets/images/restaurantour-logo.png b/assets/images/restaurantour-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e3feac4837878d604899e9930e6ceba08b1eead1 GIT binary patch literal 242977 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≫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 D7W2)h literal 0 HcmV?d00001 diff --git a/lib/core/navigation/route_navigator.dart b/lib/core/navigation/route_navigator.dart new file mode 100644 index 0000000..47a8644 --- /dev/null +++ b/lib/core/navigation/route_navigator.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:restaurant_tour/features/home_page/presenter/screen/home_screen.dart'; +import 'package:restaurant_tour/features/splash_screen/presenter/splash_screen.dart'; + +final GoRouter router = GoRouter( + initialLocation: '/splash', + routes: [ + GoRoute( + path: '/splash', + builder: (BuildContext context, GoRouterState state) => + const SplashScreen(), + ), + GoRoute( + path: '/home', + name: 'home', + builder: (BuildContext context, GoRouterState state) { + return const HomeScreen(); + }, + ), + ], +); \ No newline at end of file diff --git a/lib/features/home_page/presenter/screen/home_screen.dart b/lib/features/home_page/presenter/screen/home_screen.dart index 477473c..67d8e31 100644 --- a/lib/features/home_page/presenter/screen/home_screen.dart +++ b/lib/features/home_page/presenter/screen/home_screen.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:restaurant_tour/repositories/yelp_repository.dart'; class HomeScreen extends StatelessWidget { diff --git a/lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart b/lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart new file mode 100644 index 0000000..ca8bfb0 --- /dev/null +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart @@ -0,0 +1,19 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'splash_screen_event.dart'; +part 'splash_screen_state.dart'; + +class SplashScreenBloc extends Bloc { + SplashScreenBloc() : super(SplashScreenInitial()) { + on(_onInitialEvent); + } + + Future _onInitialEvent(InitialEvent event, Emitter emit) async { + await Future.delayed(const Duration(seconds: 2),); + + emit(const PushToHomeState()); + } +} diff --git a/lib/features/splash_screen/presenter/bloc/splash_screen_event.dart b/lib/features/splash_screen/presenter/bloc/splash_screen_event.dart new file mode 100644 index 0000000..688b2f1 --- /dev/null +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_event.dart @@ -0,0 +1,12 @@ +part of 'splash_screen_bloc.dart'; + +sealed class SplashScreenEvent extends Equatable { + const SplashScreenEvent(); +} + +class InitialEvent extends SplashScreenEvent { + const InitialEvent(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/splash_screen/presenter/bloc/splash_screen_state.dart b/lib/features/splash_screen/presenter/bloc/splash_screen_state.dart new file mode 100644 index 0000000..fd752d5 --- /dev/null +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_state.dart @@ -0,0 +1,17 @@ +part of 'splash_screen_bloc.dart'; + +sealed class SplashScreenState extends Equatable { + const SplashScreenState(); +} + +final class SplashScreenInitial extends SplashScreenState { + @override + List get props => []; +} + +class PushToHomeState extends SplashScreenState { + const PushToHomeState(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/splash_screen/presenter/splash_screen.dart b/lib/features/splash_screen/presenter/splash_screen.dart new file mode 100644 index 0000000..be310e5 --- /dev/null +++ b/lib/features/splash_screen/presenter/splash_screen.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:restaurant_tour/features/splash_screen/presenter/bloc/splash_screen_bloc.dart'; + +class SplashScreen extends StatelessWidget { + const SplashScreen({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => SplashScreenBloc()..add(const InitialEvent()), + child: _Page(), + ); + } +} + +class _Page extends StatelessWidget { + const _Page({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is PushToHomeState) { + context.goNamed('home'); + } + }, + child: const Scaffold( + body: _Body(), + ), + ); + } +} + +class _Body extends StatelessWidget { + const _Body({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Image.asset('assets/images/restaurantour-logo.png'), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index c282693..7ad0404 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,16 +1,9 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:http/http.dart' as http; -import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/home_page/presenter/screen/home_screen.dart'; -import 'package:restaurant_tour/query.dart'; - -const _apiKey = ''; -const _baseUrl = 'https://api.yelp.com/v3/graphql'; +import 'package:restaurant_tour/core/navigation/route_navigator.dart'; Future main() async { + WidgetsFlutterBinding.ensureInitialized(); await dotenv.load(fileName: ".env"); runApp( const RestaurantTour(), @@ -22,71 +15,9 @@ class RestaurantTour extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( + return MaterialApp.router( + routerConfig: router, title: 'Restaurant Tour', - home: HomeScreen(), ); } -} - -// TODO: Architect code -// This is just a POC of the API integration -// class HomePage extends StatelessWidget { -// const HomePage({super.key}); - -// Future getRestaurants({int offset = 0}) async { -// final headers = { -// 'Authorization': 'Bearer ${dotenv.env['YEP_API_KEY']}', -// 'Content-Type': 'application/graphql', -// }; - -// try { -// final response = await http.post( -// Uri.parse(_baseUrl), -// headers: headers, -// body: query(offset), -// ); - -// if (response.statusCode == 200) { -// return RestaurantQueryResult.fromJson( -// jsonDecode(response.body)['data']['search'], -// ); -// } else { -// print('Failed to load restaurants: ${response.statusCode}'); -// return null; -// } -// } catch (e) { -// print('Error fetching restaurants: $e'); -// return null; -// } -// } - -// @override -// Widget build(BuildContext context) { -// return Scaffold( -// body: Center( -// child: Column( -// mainAxisAlignment: MainAxisAlignment.center, -// children: [ -// const Text('Restaurant Tour'), -// ElevatedButton( -// child: const Text('Fetch Restaurants'), -// onPressed: () async { -// try { -// final result = await getRestaurants(); -// if (result != null) { -// print('Fetched ${result.restaurants!.length} restaurants'); -// } else { -// print('No restaurants fetched'); -// } -// } catch (e) { -// print('Failed to fetch restaurants: $e'); -// } -// }, -// ), -// ], -// ), -// ), -// ); -// } -// } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index a234e73..310d77e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + bloc: + dependency: transitive + description: + name: bloc + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" + url: "https://pub.dev" + source: hosted + version: "8.1.4" boolean_selector: dependency: transitive description: @@ -193,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -222,6 +238,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: b594505eac31a0518bdcb4b5b79573b8d9117b193cc80cc12e17d639b10aa27a + url: "https://pub.dev" + source: hosted + version: "8.1.6" flutter_dotenv: dependency: "direct main" description: @@ -243,6 +267,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -259,6 +288,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459" + url: "https://pub.dev" + source: hosted + version: "14.2.7" graphs: dependency: transitive description: @@ -395,6 +432,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + oxidized: + dependency: "direct main" + description: + name: oxidized + sha256: "5e8b0289f40b14da91159eae30d82c8603bcfaa86c3e15b5aa8f1904a08e3a7b" + url: "https://pub.dev" + source: hosted + version: "6.2.0" package_config: dependency: transitive description: @@ -419,6 +472,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" pub_semver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index aa9f207..8640232 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,11 +12,15 @@ environment: dependencies: dio: ^5.7.0 + equatable: ^2.0.5 flutter: sdk: flutter + flutter_bloc: ^8.1.6 flutter_dotenv: ^5.1.0 + go_router: ^14.2.7 http: ^1.2.2 json_annotation: ^4.9.0 + oxidized: ^6.2.0 dev_dependencies: flutter_test: @@ -30,6 +34,7 @@ flutter: uses-material-design: true assets: - .env + - assets/images/ fonts: - family: Lora fonts: From 6319fc0759593709239297a56506743f9ba81695 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 02:42:47 -0400 Subject: [PATCH 04/34] feat: layer creation for loading effect with shimmer --- lib/shared/rt_skeleton.dart | 36 ++++++++++++++++++++++++++++++++++++ pubspec.lock | 8 ++++++++ pubspec.yaml | 1 + 3 files changed, 45 insertions(+) create mode 100644 lib/shared/rt_skeleton.dart diff --git a/lib/shared/rt_skeleton.dart b/lib/shared/rt_skeleton.dart new file mode 100644 index 0000000..55397ad --- /dev/null +++ b/lib/shared/rt_skeleton.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; + +class RtSkeleton extends StatelessWidget { + const RtSkeleton({ + super.key, + required this.marginBottom, + required this.borderRadius, + required this.width, + required this.height, + }); + + final double marginBottom; + final double borderRadius; + final double width; + final double height; + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(bottom: marginBottom), + child: Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Container( + width: width, + height: height, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(borderRadius), + color: Colors.white, + ), + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 310d77e..b7e9c38 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -512,6 +512,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 8640232..e650778 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: http: ^1.2.2 json_annotation: ^4.9.0 oxidized: ^6.2.0 + shimmer: ^3.0.0 dev_dependencies: flutter_test: From 863ae7d28629076030a78beac9626893eb8b7920 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 02:52:24 -0400 Subject: [PATCH 05/34] refactor: added flutter bloc for home screen --- lib/core/navigation/route_navigator.dart | 2 +- .../presenter/bloc/home_screen_bloc.dart | 13 ++++++ .../presenter/bloc/home_screen_event.dart | 5 +++ .../presenter/bloc/home_screen_state.dart | 10 +++++ .../{screen => page}/home_screen.dart | 42 ++++++++++++++++++- 5 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 lib/features/home_page/presenter/bloc/home_screen_bloc.dart create mode 100644 lib/features/home_page/presenter/bloc/home_screen_event.dart create mode 100644 lib/features/home_page/presenter/bloc/home_screen_state.dart rename lib/features/home_page/presenter/{screen => page}/home_screen.dart (50%) diff --git a/lib/core/navigation/route_navigator.dart b/lib/core/navigation/route_navigator.dart index 47a8644..a7e4fdf 100644 --- a/lib/core/navigation/route_navigator.dart +++ b/lib/core/navigation/route_navigator.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:restaurant_tour/features/home_page/presenter/screen/home_screen.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/home_screen.dart'; import 'package:restaurant_tour/features/splash_screen/presenter/splash_screen.dart'; final GoRouter router = GoRouter( diff --git a/lib/features/home_page/presenter/bloc/home_screen_bloc.dart b/lib/features/home_page/presenter/bloc/home_screen_bloc.dart new file mode 100644 index 0000000..15bac43 --- /dev/null +++ b/lib/features/home_page/presenter/bloc/home_screen_bloc.dart @@ -0,0 +1,13 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'home_screen_event.dart'; +part 'home_screen_state.dart'; + +class HomeScreenBloc extends Bloc { + HomeScreenBloc() : super(HomeScreenInitial()) { + on((event, emit) { + // TODO: implement event handler + }); + } +} diff --git a/lib/features/home_page/presenter/bloc/home_screen_event.dart b/lib/features/home_page/presenter/bloc/home_screen_event.dart new file mode 100644 index 0000000..faa9d71 --- /dev/null +++ b/lib/features/home_page/presenter/bloc/home_screen_event.dart @@ -0,0 +1,5 @@ +part of 'home_screen_bloc.dart'; + +sealed class HomeScreenEvent extends Equatable { + const HomeScreenEvent(); +} diff --git a/lib/features/home_page/presenter/bloc/home_screen_state.dart b/lib/features/home_page/presenter/bloc/home_screen_state.dart new file mode 100644 index 0000000..8f20388 --- /dev/null +++ b/lib/features/home_page/presenter/bloc/home_screen_state.dart @@ -0,0 +1,10 @@ +part of 'home_screen_bloc.dart'; + +sealed class HomeScreenState extends Equatable { + const HomeScreenState(); +} + +final class HomeScreenInitial extends HomeScreenState { + @override + List get props => []; +} diff --git a/lib/features/home_page/presenter/screen/home_screen.dart b/lib/features/home_page/presenter/page/home_screen.dart similarity index 50% rename from lib/features/home_page/presenter/screen/home_screen.dart rename to lib/features/home_page/presenter/page/home_screen.dart index 67d8e31..19e6bc9 100644 --- a/lib/features/home_page/presenter/screen/home_screen.dart +++ b/lib/features/home_page/presenter/page/home_screen.dart @@ -1,9 +1,46 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/features/home_page/presenter/bloc/home_screen_bloc.dart'; import 'package:restaurant_tour/repositories/yelp_repository.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => HomeScreenBloc(), + child: _Page(), + ); + } +} + +class _Page extends StatelessWidget { + const _Page({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + // TODO: implement listener + }, + child: Scaffold( + appBar: AppBar( + title: const Text('RestauranTour'), + ), + body: _Body(), + ), + ); + } +} + +class _Body extends StatelessWidget { + const _Body({ + super.key, + }); + @override Widget build(BuildContext context) { return Scaffold( @@ -17,8 +54,9 @@ class HomeScreen extends StatelessWidget { final yelpRepo = YelpRepository(); try { final result = await yelpRepo.getRestaurants(); - if(result != null) { - debugPrint('Fetched ${result.restaurants!.length} restaurants'); + if (result != null) { + debugPrint( + 'Fetched ${result.restaurants!.length} restaurants'); } else { debugPrint('No restaurants fetched'); } From 0ef737de97706939b74cb234371bc0f501265cb0 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 03:16:38 -0400 Subject: [PATCH 06/34] feat: added structure of tabs for home page --- lib/core/constants.dart | 1 + .../home_page/presenter/page/home_screen.dart | 49 ++++++++++--------- .../page/widgets/all_restaurants_tab.dart | 26 ++++++++++ .../page/widgets/my_favorites_tab.dart | 26 ++++++++++ lib/shared/rt_skeleton.dart | 12 ++--- 5 files changed, 84 insertions(+), 30 deletions(-) create mode 100644 lib/core/constants.dart create mode 100644 lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart create mode 100644 lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart diff --git a/lib/core/constants.dart b/lib/core/constants.dart new file mode 100644 index 0000000..f84b0f3 --- /dev/null +++ b/lib/core/constants.dart @@ -0,0 +1 @@ +const double kPaddingTopTabBar = 32.0; \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/home_screen.dart b/lib/features/home_page/presenter/page/home_screen.dart index 19e6bc9..24b023d 100644 --- a/lib/features/home_page/presenter/page/home_screen.dart +++ b/lib/features/home_page/presenter/page/home_screen.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurant_tour/features/home_page/presenter/bloc/home_screen_bloc.dart'; -import 'package:restaurant_tour/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/widgets/all_restaurants_tab.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/widgets/my_favorites_tab.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @@ -28,7 +29,10 @@ class _Page extends StatelessWidget { }, child: Scaffold( appBar: AppBar( - title: const Text('RestauranTour'), + title: const Text( + 'RestauranTour', + style: TextStyle(fontWeight: FontWeight.w700), + ), ), body: _Body(), ), @@ -43,29 +47,26 @@ class _Body extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + toolbarHeight: 0, + bottom: const TabBar( + tabs: [ + Tab( + text: 'All Restaurants', + ), + Tab( + text: 'My Favorites', + ) + ], + ), + ), + body: const TabBarView( children: [ - const Text('Restaurant Tour'), - ElevatedButton( - onPressed: () async { - final yelpRepo = YelpRepository(); - try { - final result = await yelpRepo.getRestaurants(); - if (result != null) { - debugPrint( - 'Fetched ${result.restaurants!.length} restaurants'); - } else { - debugPrint('No restaurants fetched'); - } - } catch (e) { - debugPrint('Failed to fetch restaurants: $e'); - } - }, - child: const Text('Fetch Restaurants'), - ), + AllRestaurantsTab(), + MyFavoritesTab(), ], ), ), diff --git a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart new file mode 100644 index 0000000..027c8de --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/constants.dart'; +import 'package:restaurant_tour/shared/rt_skeleton.dart'; + +class AllRestaurantsTab extends StatelessWidget { + const AllRestaurantsTab({super.key}); + + @override + Widget build(BuildContext context) { + return const Center( + child: Padding( + padding: EdgeInsets.only(top: kPaddingTopTabBar), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + RtSkeleton( + width: 50, + height: 100, + ), + Text('All Restaurants') + ], + ), + ), + ); + } +} diff --git a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart new file mode 100644 index 0000000..44921bf --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/constants.dart'; +import 'package:restaurant_tour/shared/rt_skeleton.dart'; + + +class MyFavoritesTab extends StatelessWidget { + const MyFavoritesTab({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const Center( + child: Padding( + padding: EdgeInsets.only(top: kPaddingTopTabBar), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + RtSkeleton(height: 50, width: 100), + Text('My Favorites'), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/shared/rt_skeleton.dart b/lib/shared/rt_skeleton.dart index 55397ad..e97e5b2 100644 --- a/lib/shared/rt_skeleton.dart +++ b/lib/shared/rt_skeleton.dart @@ -4,21 +4,21 @@ import 'package:shimmer/shimmer.dart'; class RtSkeleton extends StatelessWidget { const RtSkeleton({ super.key, - required this.marginBottom, - required this.borderRadius, + this.marginBottom, + this.borderRadius, required this.width, required this.height, }); - final double marginBottom; - final double borderRadius; + final double? marginBottom; + final double? borderRadius; final double width; final double height; @override Widget build(BuildContext context) { return Container( - margin: EdgeInsets.only(bottom: marginBottom), + margin: EdgeInsets.only(bottom: marginBottom ?? 0), child: Shimmer.fromColors( baseColor: Colors.grey.shade300, highlightColor: Colors.grey.shade100, @@ -26,7 +26,7 @@ class RtSkeleton extends StatelessWidget { width: width, height: height, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(borderRadius), + borderRadius: BorderRadius.circular(borderRadius ?? 0), color: Colors.white, ), ), From df5a7bede8af338d331112d9fbfb6feec7665b4e Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 03:56:54 -0400 Subject: [PATCH 07/34] feat: added skeleton card loaded --- .../home_page/presenter/page/home_screen.dart | 48 +++++++++++-------- .../page/widgets/all_restaurants_tab.dart | 10 ++-- .../widgets/restaurant_card_skeleton.dart | 19 ++++++++ lib/shared/rt_skeleton.dart | 12 ++--- 4 files changed, 60 insertions(+), 29 deletions(-) create mode 100644 lib/features/home_page/presenter/page/widgets/restaurant_card_skeleton.dart diff --git a/lib/features/home_page/presenter/page/home_screen.dart b/lib/features/home_page/presenter/page/home_screen.dart index 24b023d..e0a8a91 100644 --- a/lib/features/home_page/presenter/page/home_screen.dart +++ b/lib/features/home_page/presenter/page/home_screen.dart @@ -47,28 +47,38 @@ class _Body extends StatelessWidget { @override Widget build(BuildContext context) { - return DefaultTabController( + return const DefaultTabController( length: 2, - child: Scaffold( - appBar: AppBar( - toolbarHeight: 0, - bottom: const TabBar( - tabs: [ - Tab( - text: 'All Restaurants', + child: Column( + children: [ + Material( + elevation: 6.0, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + child: TabBar( + indicator: UnderlineTabIndicator( + borderSide: BorderSide( + color: Colors.black, + width: 2.0, + ), + ), + labelColor: Colors.black, + unselectedLabelColor: Colors.grey, + tabs: [ + Tab(text: 'All Restaurants'), + Tab(text: 'My Favorites'), + ], ), - Tab( - text: 'My Favorites', - ) - ], + ), ), - ), - body: const TabBarView( - children: [ - AllRestaurantsTab(), - MyFavoritesTab(), - ], - ), + ], ), ); } diff --git a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart index 027c8de..ccf88ea 100644 --- a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart +++ b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/constants.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/widgets/restaurant_card_skeleton.dart'; import 'package:restaurant_tour/shared/rt_skeleton.dart'; class AllRestaurantsTab extends StatelessWidget { @@ -13,10 +14,11 @@ class AllRestaurantsTab extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - RtSkeleton( - width: 50, - height: 100, - ), + RestaurantCardSkeleton(), + RestaurantCardSkeleton(), + RestaurantCardSkeleton(), + RestaurantCardSkeleton(), + Text('All Restaurants') ], ), diff --git a/lib/features/home_page/presenter/page/widgets/restaurant_card_skeleton.dart b/lib/features/home_page/presenter/page/widgets/restaurant_card_skeleton.dart new file mode 100644 index 0000000..8436fbd --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/restaurant_card_skeleton.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/shared/rt_skeleton.dart'; + +class RestaurantCardSkeleton extends StatelessWidget { + const RestaurantCardSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Card( + child: RtSkeleton( + height: 120, + width: MediaQuery.sizeOf(context).width * 0.95, + ), + ), + ); + } +} diff --git a/lib/shared/rt_skeleton.dart b/lib/shared/rt_skeleton.dart index e97e5b2..58dd405 100644 --- a/lib/shared/rt_skeleton.dart +++ b/lib/shared/rt_skeleton.dart @@ -4,21 +4,21 @@ import 'package:shimmer/shimmer.dart'; class RtSkeleton extends StatelessWidget { const RtSkeleton({ super.key, - this.marginBottom, - this.borderRadius, + this.marginBottom = 0.0, + this.borderRadius = 12.0, required this.width, required this.height, }); - final double? marginBottom; - final double? borderRadius; + final double marginBottom; + final double borderRadius; final double width; final double height; @override Widget build(BuildContext context) { return Container( - margin: EdgeInsets.only(bottom: marginBottom ?? 0), + margin: EdgeInsets.only(bottom: marginBottom), child: Shimmer.fromColors( baseColor: Colors.grey.shade300, highlightColor: Colors.grey.shade100, @@ -26,7 +26,7 @@ class RtSkeleton extends StatelessWidget { width: width, height: height, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(borderRadius ?? 0), + borderRadius: BorderRadius.circular(borderRadius), color: Colors.white, ), ), From f262483f133ed83694f9487987fb16403c3a6529 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 09:27:26 -0400 Subject: [PATCH 08/34] refactor: TabView widget splitted in smaller components --- .../home_page/presenter/bloc/home_bloc.dart | 24 +++++++++++ .../home_page/presenter/bloc/home_event.dart | 13 ++++++ .../presenter/bloc/home_screen_bloc.dart | 13 ------ .../presenter/bloc/home_screen_event.dart | 5 --- .../presenter/bloc/home_screen_state.dart | 10 ----- .../home_page/presenter/bloc/home_state.dart | 15 +++++++ .../home_page/presenter/page/home_screen.dart | 43 +++++++++++-------- .../page/widgets/all_restaurants_tab.dart | 5 --- .../page/widgets/home_loading_skeleton.dart | 18 ++++++++ .../page/widgets/my_favorites_tab.dart | 1 - .../presenter/page/widgets/tab_views.dart | 26 +++++++++++ 11 files changed, 121 insertions(+), 52 deletions(-) create mode 100644 lib/features/home_page/presenter/bloc/home_bloc.dart create mode 100644 lib/features/home_page/presenter/bloc/home_event.dart delete mode 100644 lib/features/home_page/presenter/bloc/home_screen_bloc.dart delete mode 100644 lib/features/home_page/presenter/bloc/home_screen_event.dart delete mode 100644 lib/features/home_page/presenter/bloc/home_screen_state.dart create mode 100644 lib/features/home_page/presenter/bloc/home_state.dart create mode 100644 lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart create mode 100644 lib/features/home_page/presenter/page/widgets/tab_views.dart diff --git a/lib/features/home_page/presenter/bloc/home_bloc.dart b/lib/features/home_page/presenter/bloc/home_bloc.dart new file mode 100644 index 0000000..8936fc6 --- /dev/null +++ b/lib/features/home_page/presenter/bloc/home_bloc.dart @@ -0,0 +1,24 @@ +import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'home_event.dart'; +part 'home_state.dart'; + +class HomeBloc extends Bloc { + HomeBloc() : super(HomeInitial()) { + on((event, emit) { + // TODO: implement event handler + }); + on(_onInitialEvent); + } + + Future _onInitialEvent( + InitialEvent event, + Emitter emit, + ) async { + emit( + HomeLoadingState(), + ); + } +} \ No newline at end of file diff --git a/lib/features/home_page/presenter/bloc/home_event.dart b/lib/features/home_page/presenter/bloc/home_event.dart new file mode 100644 index 0000000..6ed1979 --- /dev/null +++ b/lib/features/home_page/presenter/bloc/home_event.dart @@ -0,0 +1,13 @@ +part of 'home_bloc.dart'; + +sealed class HomeEvent extends Equatable { + const HomeEvent(); +} + + +class InitialEvent extends HomeEvent { + const InitialEvent(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/home_page/presenter/bloc/home_screen_bloc.dart b/lib/features/home_page/presenter/bloc/home_screen_bloc.dart deleted file mode 100644 index 15bac43..0000000 --- a/lib/features/home_page/presenter/bloc/home_screen_bloc.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:equatable/equatable.dart'; - -part 'home_screen_event.dart'; -part 'home_screen_state.dart'; - -class HomeScreenBloc extends Bloc { - HomeScreenBloc() : super(HomeScreenInitial()) { - on((event, emit) { - // TODO: implement event handler - }); - } -} diff --git a/lib/features/home_page/presenter/bloc/home_screen_event.dart b/lib/features/home_page/presenter/bloc/home_screen_event.dart deleted file mode 100644 index faa9d71..0000000 --- a/lib/features/home_page/presenter/bloc/home_screen_event.dart +++ /dev/null @@ -1,5 +0,0 @@ -part of 'home_screen_bloc.dart'; - -sealed class HomeScreenEvent extends Equatable { - const HomeScreenEvent(); -} diff --git a/lib/features/home_page/presenter/bloc/home_screen_state.dart b/lib/features/home_page/presenter/bloc/home_screen_state.dart deleted file mode 100644 index 8f20388..0000000 --- a/lib/features/home_page/presenter/bloc/home_screen_state.dart +++ /dev/null @@ -1,10 +0,0 @@ -part of 'home_screen_bloc.dart'; - -sealed class HomeScreenState extends Equatable { - const HomeScreenState(); -} - -final class HomeScreenInitial extends HomeScreenState { - @override - List get props => []; -} diff --git a/lib/features/home_page/presenter/bloc/home_state.dart b/lib/features/home_page/presenter/bloc/home_state.dart new file mode 100644 index 0000000..752802f --- /dev/null +++ b/lib/features/home_page/presenter/bloc/home_state.dart @@ -0,0 +1,15 @@ +part of 'home_bloc.dart'; + +sealed class HomeState extends Equatable { + const HomeState(); +} + +final class HomeInitial extends HomeState { + @override + List get props => []; +} + +class HomeLoadingState extends HomeState { + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/home_screen.dart b/lib/features/home_page/presenter/page/home_screen.dart index e0a8a91..1144f5c 100644 --- a/lib/features/home_page/presenter/page/home_screen.dart +++ b/lib/features/home_page/presenter/page/home_screen.dart @@ -1,16 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurant_tour/features/home_page/presenter/bloc/home_screen_bloc.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/widgets/all_restaurants_tab.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/widgets/my_favorites_tab.dart'; +import 'package:restaurant_tour/features/home_page/presenter/bloc/home_bloc.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/widgets/home_loading_skeleton.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/widgets/tab_views.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => HomeScreenBloc(), + return BlocProvider( + create: (context) => HomeBloc()..add(const InitialEvent()), child: _Page(), ); } @@ -23,7 +23,7 @@ class _Page extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocListener( + return BlocListener( listener: (context, state) { // TODO: implement listener }, @@ -47,17 +47,15 @@ class _Body extends StatelessWidget { @override Widget build(BuildContext context) { - return const DefaultTabController( + return DefaultTabController( length: 2, - child: Column( - children: [ - Material( - elevation: 6.0, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10.0), - topRight: Radius.circular(10.0), - ), - child: ClipRRect( + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Material( + elevation: 6.0, borderRadius: BorderRadius.only( topLeft: Radius.circular(10.0), topRight: Radius.circular(10.0), @@ -77,8 +75,17 @@ class _Body extends StatelessWidget { ], ), ), - ), - ], + BlocBuilder( + builder: (context, state) { + if (state is HomeLoadingState) { + return const HomeLoadingSkeleton(); + } else { + return const TabViews(); + } + }, + ), + ], + ), ), ); } diff --git a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart index ccf88ea..e8e94d2 100644 --- a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart +++ b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart @@ -14,11 +14,6 @@ class AllRestaurantsTab extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - RestaurantCardSkeleton(), - RestaurantCardSkeleton(), - RestaurantCardSkeleton(), - RestaurantCardSkeleton(), - Text('All Restaurants') ], ), diff --git a/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart b/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart new file mode 100644 index 0000000..8809a70 --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/widgets/restaurant_card_skeleton.dart'; + +class HomeLoadingSkeleton extends StatelessWidget { + const HomeLoadingSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + children: [ + RestaurantCardSkeleton(), + RestaurantCardSkeleton(), + RestaurantCardSkeleton(), + RestaurantCardSkeleton(), + ], + ); + } +} diff --git a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart index 44921bf..9d98b9b 100644 --- a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart +++ b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart @@ -16,7 +16,6 @@ class MyFavoritesTab extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - RtSkeleton(height: 50, width: 100), Text('My Favorites'), ], ), diff --git a/lib/features/home_page/presenter/page/widgets/tab_views.dart b/lib/features/home_page/presenter/page/widgets/tab_views.dart new file mode 100644 index 0000000..3ac2d15 --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/tab_views.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/widgets/all_restaurants_tab.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/widgets/my_favorites_tab.dart'; + + +class TabViews extends StatelessWidget { + const TabViews({super.key}); + + @override + Widget build(BuildContext context) { + return const Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 200, + child: TabBarView( + children: [ + AllRestaurantsTab(), + MyFavoritesTab(), + ], + ), + ), + ], + ); + } +} \ No newline at end of file From 8560989702877573678ab0aa1ec3b98d9451a2c6 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 09:35:04 -0400 Subject: [PATCH 09/34] feat: added oxidized for functional programming in yelp repository --- lib/repositories/yelp_repository.dart | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index 1fe489f..ae9db62 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/repositories/yelp_repository.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:oxidized/oxidized.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; class YelpRepository { @@ -17,16 +18,25 @@ class YelpRepository { ); } - Future getRestaurants({int offset = 0}) async { + Future> getRestaurants({int offset = 0}) async { try { final response = await dio.post>( '/v3/graphql', data: _getQuery(offset), ); - return RestaurantQueryResult.fromJson(response.data!['data']['search']); + final result = RestaurantQueryResult.fromJson(response.data!['data']['search']); + return Ok(result); } catch (e) { - print('Error fetching restaurants: $e'); - return null; + if(e is DioException) { + return Err(e); + } else { + return Err( + DioException( + requestOptions: RequestOptions(path: '/v3/graphql'), + error: e, + ), + ); + } } } From bffb5b0b07db6a6625d0adb6180002803ffc51a8 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 09:46:03 -0400 Subject: [PATCH 10/34] feat: added null safety to json serializable objects in model restaurant --- lib/core/models/restaurant.dart | 15 ++- lib/core/models/restaurant.g.dart | 10 +- pubspec.lock | 156 +++++++++++++++--------------- 3 files changed, 95 insertions(+), 86 deletions(-) diff --git a/lib/core/models/restaurant.dart b/lib/core/models/restaurant.dart index 1c7ad2f..74fd69f 100644 --- a/lib/core/models/restaurant.dart +++ b/lib/core/models/restaurant.dart @@ -143,15 +143,22 @@ class Restaurant { class RestaurantQueryResult { final int? total; @JsonKey(name: 'business') - final List? restaurants; + final List restaurants; const RestaurantQueryResult({ this.total, - this.restaurants, + required this.restaurants, }); - factory RestaurantQueryResult.fromJson(Map json) => - _$RestaurantQueryResultFromJson(json); + factory RestaurantQueryResult.fromJson(Map json) { + return RestaurantQueryResult( + total: json['total'] as int?, + restaurants: (json['business'] as List?) + ?.map((e) => Restaurant.fromJson(e as Map)) + .toList() ?? + [], + ); + } Map toJson() => _$RestaurantQueryResultToJson(this); } diff --git a/lib/core/models/restaurant.g.dart b/lib/core/models/restaurant.g.dart index 3ed33f9..e365367 100644 --- a/lib/core/models/restaurant.g.dart +++ b/lib/core/models/restaurant.g.dart @@ -38,15 +38,17 @@ Map _$UserToJson(User instance) => { Review _$ReviewFromJson(Map json) => Review( id: json['id'] as String?, - rating: json['rating'] as int?, + rating: (json['rating'] as num?)?.toInt(), user: json['user'] == null ? null : User.fromJson(json['user'] as Map), + text: json['text'] as String?, ); Map _$ReviewToJson(Review instance) => { 'id': instance.id, 'rating': instance.rating, + 'text': instance.text, 'user': instance.user, }; @@ -95,9 +97,9 @@ Map _$RestaurantToJson(Restaurant instance) => RestaurantQueryResult _$RestaurantQueryResultFromJson( Map json) => RestaurantQueryResult( - total: json['total'] as int?, - restaurants: (json['business'] as List?) - ?.map((e) => Restaurant.fromJson(e as Map)) + total: (json['total'] as num?)?.toInt(), + restaurants: (json['business'] as List) + .map((e) => Restaurant.fromJson(e as Map)) .toList(), ); diff --git a/pubspec.lock b/pubspec.lock index b7e9c38..76d1065 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.4.1" args: dependency: transitive description: name: args - sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" async: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -69,18 +69,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: f4d6244cc071ba842c296cb1c4ee1b31596b9f924300647ac7a1445493471a3f + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe url: "https://pub.dev" source: hosted - version: "7.2.3" + version: "7.3.1" built_collection: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: built_value - sha256: b6c9911b2d670376918d5b8779bc27e0e612a94ec3ff0343689e991d8d0a3b8a + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "8.9.2" characters: dependency: transitive description: @@ -121,22 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 - url: "https://pub.dev" - source: hosted - version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" clock: dependency: transitive description: @@ -165,26 +157,26 @@ packages: dependency: transitive description: name: convert - sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.5" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.6" dio: dependency: "direct main" description: @@ -221,18 +213,18 @@ packages: dependency: transitive description: name: file - sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "7.0.0" fixnum: dependency: transitive description: name: fixnum - sha256: "6a2ef17156f4dc49684f9d99aaf4a93aba8ac49f5eac861755f5730ddf6e2e4e" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -276,18 +268,18 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: name: glob - sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" go_router: dependency: "direct main" description: @@ -300,10 +292,10 @@ packages: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" http: dependency: "direct main" description: @@ -316,34 +308,34 @@ packages: dependency: transitive description: name: http_multi_server - sha256: bfb651625e251a88804ad6d596af01ea903544757906addcb2dcdf088b5ea185 + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" io: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.1" json_annotation: dependency: "direct main" description: @@ -396,10 +388,10 @@ packages: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" matcher: dependency: transitive description: @@ -428,10 +420,10 @@ packages: dependency: transitive description: name: mime - sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.6" nested: dependency: transitive description: @@ -452,10 +444,10 @@ packages: dependency: transitive description: name: package_config - sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: transitive description: @@ -468,10 +460,10 @@ packages: dependency: transitive description: name: pool - sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" provider: dependency: transitive description: @@ -484,34 +476,34 @@ packages: dependency: transitive description: name: pub_semver - sha256: b5a5fcc6425ea43704852ba4453ba94b08c2226c63418a260240c3a054579014 + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "3686efe4a4613a4449b1a4ae08670aadbd3376f2e78d93e3f8f0919db02a7256" + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" shelf: dependency: transitive description: name: shelf - sha256: c240984c924796e055e831a0a36db23be8cb04f170b26df572931ab36418421d + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: fd84910bf7d58db109082edf7326b75322b8f186162028482f53dc892f00332d + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.0" shimmer: dependency: "direct main" description: @@ -569,10 +561,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -601,18 +593,18 @@ packages: dependency: transitive description: name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" vector_math: dependency: transitive description: @@ -633,10 +625,10 @@ packages: dependency: transitive description: name: watcher - sha256: e42dfcc48f67618344da967b10f62de57e04bae01d9d3af4c2596f3712a88c99 + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" web: dependency: transitive description: @@ -645,22 +637,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "3.0.1" yaml: dependency: transitive description: name: yaml - sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" sdks: dart: ">=3.4.0 <4.0.0" flutter: ">=3.19.6" From fd3797abe97cefce9d9765c66ddb717007a80f07 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 09:52:11 -0400 Subject: [PATCH 11/34] refactor: functional programming implementation in home bloc, screen --- .../home_page/presenter/bloc/home_bloc.dart | 24 ++++++++++++++--- .../home_page/presenter/bloc/home_state.dart | 27 ++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/lib/features/home_page/presenter/bloc/home_bloc.dart b/lib/features/home_page/presenter/bloc/home_bloc.dart index 8936fc6..3e43a00 100644 --- a/lib/features/home_page/presenter/bloc/home_bloc.dart +++ b/lib/features/home_page/presenter/bloc/home_bloc.dart @@ -1,15 +1,14 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/repositories/yelp_repository.dart'; part 'home_event.dart'; part 'home_state.dart'; class HomeBloc extends Bloc { HomeBloc() : super(HomeInitial()) { - on((event, emit) { - // TODO: implement event handler - }); on(_onInitialEvent); } @@ -17,8 +16,27 @@ class HomeBloc extends Bloc { InitialEvent event, Emitter emit, ) async { + final yelpRepo = YelpRepository(); + final result = await yelpRepo.getRestaurants(); + emit( HomeLoadingState(), ); + result.when( + ok: (data) { + if (data.restaurants.isNotEmpty) { + emit( + HomeDataLoadedState( + restaurantList: data.restaurants, + ), + ); + } else { + emit(const HomeEmptyDataState()); + } + }, + err: (error) { + emit(ErrorState(error: error.toString())); + }, + ); } } \ No newline at end of file diff --git a/lib/features/home_page/presenter/bloc/home_state.dart b/lib/features/home_page/presenter/bloc/home_state.dart index 752802f..b62b0bb 100644 --- a/lib/features/home_page/presenter/bloc/home_state.dart +++ b/lib/features/home_page/presenter/bloc/home_state.dart @@ -12,4 +12,29 @@ final class HomeInitial extends HomeState { class HomeLoadingState extends HomeState { @override List get props => []; -} \ No newline at end of file +} + +class HomeDataLoadedState extends HomeState { + const HomeDataLoadedState({required this.restaurantList}); + + final List restaurantList; + + @override + List get props => [restaurantList]; +} + +class HomeEmptyDataState extends HomeState { + const HomeEmptyDataState(); + + @override + List get props => []; +} + +class ErrorState extends HomeState { + const ErrorState({required this.error}); + + final String error; + + @override + List get props => [error]; +} From ac24dcf722a5fe4db1bcaf701f543dbb553e2191 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 09:59:02 -0400 Subject: [PATCH 12/34] feat: added loading indicator in data fetch home screen --- assets/restaurants.json | 1197 +++++++++++++++++ .../home_page/presenter/page/home_screen.dart | 68 +- .../presenter/page/widgets/tab_views.dart | 11 +- 3 files changed, 1239 insertions(+), 37 deletions(-) create mode 100644 assets/restaurants.json diff --git a/assets/restaurants.json b/assets/restaurants.json new file mode 100644 index 0000000..de38f79 --- /dev/null +++ b/assets/restaurants.json @@ -0,0 +1,1197 @@ +{ + "data": { + "search": { + "total": 6184, + "business": [ + { + "id": "vHz2RLtfUMVRPFmd7VBEHA", + "name": "Gordon Ramsay Hell's Kitchen", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg" + ], + "reviews": [ + { + "id": "DKtLdByPmlwZET_b4BM3gQ", + "rating": 5, + "user": { + "id": "dW0QJVcKiX7crMd1lYWTkg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/yhQgs5pEXcKaSRxVaY9z6w/o.jpg", + "name": "Misty C." + } + }, + { + "id": "PdS4Fv6RKyBQ1nB0L0wpsg", + "rating": 5, + "user": { + "id": "TVnNlNYw5uFp-D-lv9REXA", + "image_url": null, + "name": "Chubby T." + } + }, + { + "id": "9rADlcW-gfmu-F_bHK6WOw", + "rating": 3, + "user": { + "id": "YKh3b-qojo4vjtHEZoIjKA", + "image_url": null, + "name": "Jeheon L." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Seafood", + "alias": "seafood" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3570 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "QXV3L_QFGj8r6nWX2kS2hA", + "name": "Nacho Daddy", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media4.fl.yelpcdn.com/bphoto/pu9doqMplB5x5SEs8ikW6w/o.jpg" + ], + "reviews": [ + { + "id": "DQ2H8OgyBTbe6jN5LqGXdA", + "rating": 5, + "user": { + "id": "oufmvIs63kYDNT4LFy-mzA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/FgemZl9aSNbb6EPcAG-jbw/o.jpg", + "name": "Nastacia M." + } + }, + { + "id": "0u-2PXiNc_ugmyUwOx8B5w", + "rating": 5, + "user": { + "id": "LcN1aD-HHqCNlWzqTPfl6g", + "image_url": null, + "name": "Nataly E." + } + }, + { + "id": "81RGgDCGWK9DOF8xf9wTBA", + "rating": 4, + "user": { + "id": "ade13lGTtnC25U57AKRW_A", + "image_url": null, + "name": "Michael m." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3663 Las Vegas Blvd\nSte 595\nLas Vegas, NV 89109" + } + }, + { + "id": "faPVqws-x-5k2CQKDNtHxw", + "name": "Yardbird", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/xYJaanpF3Dl1OovhmpqAYw/o.jpg" + ], + "reviews": [ + { + "id": "jF_ltrsWELOE3J62CfjVOA", + "rating": 5, + "user": { + "id": "L6R9AgLVcYZRex-zD2dyGQ", + "image_url": null, + "name": "Rodil A." + } + }, + { + "id": "IN-fzeDTSemdZjOlBSW-Xw", + "rating": 5, + "user": { + "id": "goYizeAZdZbQhZZE5QOe8w", + "image_url": null, + "name": "Cliff G." + } + }, + { + "id": "x9RWVj4xZdV_oep2i6c1sA", + "rating": 5, + "user": { + "id": "PY9912npDSkcfO3He7bosQ", + "image_url": null, + "name": "Hiyori G." + } + } + ], + "categories": [ + { + "title": "Southern", + "alias": "southern" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3355 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "syhA1ugJpyNLaB0MiP19VA", + "name": "888 Japanese BBQ", + "price": "$$$", + "rating": 4.8, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/V_zmwCUG1o_vR29xfkb-ng/o.jpg" + ], + "reviews": [ + { + "id": "QKuvkV1Tb-d14-Hfo6KkGw", + "rating": 4, + "user": { + "id": "R_DrrfxzKvQtVpgIv1KXjw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/FHwSndIBTpNLIoU99Qsozg/o.jpg", + "name": "Grace D." + } + }, + { + "id": "LKSWKmpe4p6XwM2_GTK_tg", + "rating": 5, + "user": { + "id": "xcF1SCYEtj9OK3TwYqV5Qg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/esVhZkLVrWtuXBPgJ6sUjw/o.jpg", + "name": "felicia J." + } + }, + { + "id": "foPmGbRnFmALLevmXgGN6w", + "rating": 5, + "user": { + "id": "o14GLSjW4a6L_5dofmfbTw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/7lpT74I1nVghDStisoT9mQ/o.jpg", + "name": "Yichu W." + } + } + ], + "categories": [ + { + "title": "Barbeque", + "alias": "bbq" + }, + { + "title": "Japanese", + "alias": "japanese" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3550 S Decatur Blvd\nLas Vegas, NV 89103" + } + }, + { + "id": "2iTsRqUsPGRH1li1WVRvKQ", + "name": "Carson Kitchen", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/LhaPvLHIrsHu8ZMLgV04OQ/o.jpg" + ], + "reviews": [ + { + "id": "sZVa1-2TWjgJEnKGJYYB4Q", + "rating": 5, + "user": { + "id": "Poe6Ka98uk2V3FTH25gmVQ", + "image_url": null, + "name": "Cynthia D." + } + }, + { + "id": "5t4my7iYtsLNUO8x-SSUsw", + "rating": 5, + "user": { + "id": "37DUcB2WAP5CF99T1bLsGw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/24vNaKwJjGmhdl-B5tedhw/o.jpg", + "name": "Justin G." + } + }, + { + "id": "1PKEZpeVRgb05RihejOJIw", + "rating": 3, + "user": { + "id": "z6EDB2Y_ArgnhYOaL68KhA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/8N1nVMkzR8jachxvpswCKg/o.jpg", + "name": "Chastina S." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Desserts", + "alias": "desserts" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "124 S 6th St\nSte 100\nLas Vegas, NV 89101" + } + }, + { + "id": "4JNXUYY8wbaaDmk3BPzlWw", + "name": "Mon Ami Gabi", + "price": "$$$", + "rating": 4.2, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/cZ75DtuiHsOU-4W3vLsFKA/o.jpg" + ], + "reviews": [ + { + "id": "nWWGiAcfUV4fMTG1iZwLDg", + "rating": 4, + "user": { + "id": "WG7jNZ6T2s74xaCVvAqrNQ", + "image_url": null, + "name": "Jayton W." + } + }, + { + "id": "9U8FJ8JAqpVKqIzvSrNwbw", + "rating": 5, + "user": { + "id": "HGgsNBaaUprlK8kbGN1Xmg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/ct4hxqGIHJKP4wssXduKuQ/o.jpg", + "name": "Lisa C." + } + }, + { + "id": "lcQCPO_F7R0vIUQTbkE2Zw", + "rating": 4, + "user": { + "id": "OLn8EvPsu4hNug8V5PF2jA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/xpr7Du-c8rZ9G4Tc00M7ig/o.jpg", + "name": "Rachel S." + } + } + ], + "categories": [ + { + "title": "French", + "alias": "french" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3655 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "rdE9gg0WB7Z8kRytIMSapg", + "name": "Lazy Dog Restaurant & Bar", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/_Wz-fNXawmbBinSf9Ev15g/o.jpg" + ], + "reviews": [ + { + "id": "DdLrH47JOtFWBgERXSQdiw", + "rating": 5, + "user": { + "id": "5stRmR9p3vREwvtS-S81zg", + "image_url": null, + "name": "Rick H." + } + }, + { + "id": "OAgIc_8QG6rS5o7nVBFipg", + "rating": 5, + "user": { + "id": "lhEdvKMSzT9NvP0AsZ8PeA", + "image_url": null, + "name": "Cameron L." + } + }, + { + "id": "GSY-WHs9PHayK6BTQD7QyA", + "rating": 5, + "user": { + "id": "quXARBB0TFNxwHrTFPle4A", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/UnVkj8_uayckUSyOnxSeYg/o.jpg", + "name": "Renée H." + } + } + ], + "categories": [ + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Comfort Food", + "alias": "comfortfood" + }, + { + "title": "Burgers", + "alias": "burgers" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "6509 S Las Vegas Blvd\nLas Vegas, NV 89119" + } + }, + { + "id": "JPfi__QJAaRzmfh5aOyFEw", + "name": "Shang Artisan Noodle", + "price": "$$", + "rating": 4.6, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/TqV2TDWH-7Wje5B9Oh1EZw/o.jpg" + ], + "reviews": [ + { + "id": "to7hZMQ5ait363QdwZWObQ", + "rating": 4, + "user": { + "id": "mjSQELtcLOf55ij-JQagvw", + "image_url": null, + "name": "Eric Y." + } + }, + { + "id": "1kR1sYXsQ-P34OUX_7dfTA", + "rating": 5, + "user": { + "id": "46MOzJsXEi6bNeiiKdf87g", + "image_url": null, + "name": "Renee S." + } + }, + { + "id": "BM4hmLR1nzafikmIdjTVSA", + "rating": 5, + "user": { + "id": "QCfSyRowk0f6Po78n-R91Q", + "image_url": null, + "name": "Eileen L." + } + } + ], + "categories": [ + { + "title": "Noodles", + "alias": "noodles" + }, + { + "title": "Chinese", + "alias": "chinese" + }, + { + "title": "Soup", + "alias": "soup" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "4983 W Flamingo Rd\nSte B\nLas Vegas, NV 89103" + } + }, + { + "id": "UidEFF1WpnU4duev4fjPlQ", + "name": "Therapy ", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/otaMuPtauoEb6qZzmHlAlQ/o.jpg" + ], + "reviews": [ + { + "id": "PsR_yQOXt_w8PUkTGlBjkA", + "rating": 5, + "user": { + "id": "VmSDPCypfNRYJL6iMXqQZQ", + "image_url": null, + "name": "Zoe C." + } + }, + { + "id": "t5KE0YZKeRGxX8TLl17SVw", + "rating": 5, + "user": { + "id": "xhC7iVSHkf9pdXu2NVDAhA", + "image_url": null, + "name": "Grant W." + } + }, + { + "id": "49zIJLuJkZRj460MfGAj6A", + "rating": 5, + "user": { + "id": "Qb_YdQd6IdogNBzCnSu5bw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/SyE0UJtWlVo9I1BKZbzrfA/o.jpg", + "name": "Jessamyn C." + } + } + ], + "categories": [ + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "New American", + "alias": "newamerican" + }, + { + "title": "Dance Clubs", + "alias": "danceclubs" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "518 Fremont St\nLas Vegas, NV 89101" + } + }, + { + "id": "SAIrNOB4PtDA4gziNCucwg", + "name": "Herbs & Rye", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/TlVbVAAP0aNH9BOu9APtzA/o.jpg" + ], + "reviews": [ + { + "id": "B97o7gl-PU25qHAcTB3FEg", + "rating": 5, + "user": { + "id": "qPztOGTqm2IthTL1xACWBA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/jxknegv17uywfwR8_nwclg/o.jpg", + "name": "Vanessa B." + } + }, + { + "id": "JI7kuEd7jedjn76_QS2LCg", + "rating": 4, + "user": { + "id": "8pzzXEPoZxuB1mjhMlgX9A", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/5N84QoGaADOVxZ4J4EFjjQ/o.jpg", + "name": "Wanna L." + } + }, + { + "id": "QpsMv6UA6_ACEuU3rCXVnA", + "rating": 5, + "user": { + "id": "8NK7qotYwhAPBcNmUy7uCQ", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/YAgv-g2SwndzAv001M-4xQ/o.jpg", + "name": "Letitia H." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3713 W Sahara Ave\nLas Vegas, NV 89102" + } + }, + { + "id": "I6EDDi4-Eq_XlFghcDCUhw", + "name": "Joe's Seafood Prime Steak & Stone Crab", + "price": "$$$", + "rating": 4.4, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/I1GDdV1mWUJM5HTP1PIX6A/o.jpg" + ], + "reviews": [ + { + "id": "ccaHPa-J9zx7FORUUGDbjA", + "rating": 5, + "user": { + "id": "tQDSfuYHzrQyUhC0GT5mGA", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/XweFm7ELT2clB3MYpxeA5Q/o.jpg", + "name": "Cindy R." + } + }, + { + "id": "usmjsEE_lsLNsyWVl16P2g", + "rating": 4, + "user": { + "id": "TFh8SgmdlGor2sdv7V70rQ", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/HUaAs1PDjnmtqGLbiqnrlQ/o.jpg", + "name": "R R." + } + }, + { + "id": "wFpymrx6ROYU-ZXR5cnUxQ", + "rating": 5, + "user": { + "id": "vMehw15-3PXzhvx0XYXEVA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/lxbYBzWHqDOUAZoWw5Rwhg/o.jpg", + "name": "Nick K. W." + } + } + ], + "categories": [ + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3500 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "wmId49_BwzfWd3ww6GDMeA", + "name": "Cleaver - Butchered Meats, Seafood & Cocktails", + "price": "$$$", + "rating": 4.5, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/NVZAUiFMQ6gACo7IOwOFAA/o.jpg" + ], + "reviews": [ + { + "id": "jhvXG-TcrSU-4dovEOsHxQ", + "rating": 5, + "user": { + "id": "OZxbB3Rtq0yMzjSu_fRkLw", + "image_url": null, + "name": "Warren S." + } + }, + { + "id": "1VvE3TB3beLoi3qg0Iws1g", + "rating": 5, + "user": { + "id": "nPGssW_jVwcmExmbG_4Vig", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/d_tJRaVqqrSAeWVioaIkwg/o.jpg", + "name": "Aleksandra T." + } + }, + { + "id": "55mFi3cKeoiGxHldDqVfTA", + "rating": 5, + "user": { + "id": "iW-mip0SpyteujfjfFNmhg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/3figr-Wy-9IRPGhufDT9nA/o.jpg", + "name": "Ngoc M." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Seafood", + "alias": "seafood" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3900 Paradise Rd\nSte D1\nLas Vegas, NV 89169" + } + }, + { + "id": "nUpz0YiBsOK7ff9k3vUJ3A", + "name": "Buddy V's Ristorante", + "price": "$$", + "rating": 4.2, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/gLHjQg0bjGjr_Jus-BXqDA/o.jpg" + ], + "reviews": [ + { + "id": "Ei37fwQISHjcW7Flq0lM0g", + "rating": 5, + "user": { + "id": "p9Yn8XDkIcCawrOBHfE5iA", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/o-9aQP_ZN2xTxhVlkq5lUw/o.jpg", + "name": "Vanessa H." + } + }, + { + "id": "rDt1nlgRtI3ASYYz_cwbrQ", + "rating": 4, + "user": { + "id": "wKRaCZvy046AldtzflczaQ", + "image_url": null, + "name": "Linda H." + } + }, + { + "id": "VwUtf3nVQsdobgRiOIxKqw", + "rating": 5, + "user": { + "id": "pCQ8urlykb8VRNm5IjJSWg", + "image_url": "https://s3-media4.fl.yelpcdn.com/photo/o36eZXAvfV5y7Ww-LyGkig/o.jpg", + "name": "Ian M." + } + } + ], + "categories": [ + { + "title": "Italian", + "alias": "italian" + }, + { + "title": "American", + "alias": "tradamerican" + }, + { + "title": "Wine Bars", + "alias": "wine_bars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3327 S Las Vegas Blvd\nLas Vegas, NV 89109" + } + }, + { + "id": "JDZ6_yycNQFTpUZzLIKHUg", + "name": "El Dorado Cantina - Las Vegas Strip", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/XUohVZ4cdk13GWrUmnQKYQ/o.jpg" + ], + "reviews": [ + { + "id": "i2gXEIKJ045uUEdaZbZ_Zw", + "rating": 5, + "user": { + "id": "LhyepAmUttTm5suU_MoECQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/v-Pi6w7g8CdyzTr_6_IsEQ/o.jpg", + "name": "Cheyenne L." + } + }, + { + "id": "nDUQX9fBRtfi0VTLrStN6g", + "rating": 5, + "user": { + "id": "zPf0o5w4LH5vm5iF2Clpkg", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/-reqGCP7l1tqB2KndJ-8LA/o.jpg", + "name": "Christina K." + } + }, + { + "id": "AP26RnkWGgAfF-b3I3euTg", + "rating": 3, + "user": { + "id": "HITN4vuuhFZpSIlf4QsRvA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/zmSlyTiInVxB0gS_K0vGyQ/o.jpg", + "name": "J D." + } + } + ], + "categories": [ + { + "title": "Mexican", + "alias": "mexican" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "Latin American", + "alias": "latin" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3025 Sammy Davis Jr Dr\nLas Vegas, NV 89109" + } + }, + { + "id": "myFPRndhdZMKdfMZyksyxQ", + "name": "ITs SUSHI Spring Mountain", + "price": "$$", + "rating": 4.4, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/5OQj-6E-xC_FKJERHBQvrw/o.jpg" + ], + "reviews": [ + { + "id": "z0S_H-Rjo6xPqgK-n98ruw", + "rating": 5, + "user": { + "id": "0PT_94Yf5m6rGHSDeLd9Qw", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/cKi9FcQlkY0He23Sf0bGlQ/o.jpg", + "name": "Steven D." + } + }, + { + "id": "V_90_pt6Jtvtn8sUxfJaxw", + "rating": 5, + "user": { + "id": "rCNJWteUQ-p65f10VTuuTA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/mAthx7h4LZ6jPtopp4dQSA/o.jpg", + "name": "Brandy G." + } + }, + { + "id": "qBz2Dx-VYWPj367yyePpKQ", + "rating": 5, + "user": { + "id": "Uq0lff_TZavtuXaXmOp8ow", + "image_url": "https://s3-media3.fl.yelpcdn.com/photo/pFxHGQQVt5CunekVX6ebrg/o.jpg", + "name": "Ethan P." + } + } + ], + "categories": [ + { + "title": "Japanese", + "alias": "japanese" + }, + { + "title": "Sushi Bars", + "alias": "sushi" + }, + { + "title": "Buffets", + "alias": "buffets" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4815 Spring Mountain Rd\nLas Vegas, NV 89103" + } + }, + { + "id": "gOOfBSBZlffCkQ7dr7cpdw", + "name": "CHICA", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/FxmtjuzPDiL7vx5KyceWuQ/o.jpg" + ], + "reviews": [ + { + "id": "lW4yyq9CTIsMKM_YaD2t6Q", + "rating": 5, + "user": { + "id": "qtV2u7-cR0ueiOcObwm8EQ", + "image_url": null, + "name": "Petra R." + } + }, + { + "id": "NQeiKHUZ4u-TLNTO3zenBQ", + "rating": 5, + "user": { + "id": "_gzc2WONOcCNF8k2wobRQw", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/1zjnSzZGFSp1FqscHxqyug/o.jpg", + "name": "Raymond S." + } + }, + { + "id": "TBxZBQlFDPBsddXc0l1tdA", + "rating": 4, + "user": { + "id": "TxVTmvdbXa5kgmj9O6dRaw", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/DX06kvui2jls8rjsaZjCNg/o.jpg", + "name": "Ly T." + } + } + ], + "categories": [ + { + "title": "Latin American", + "alias": "latin" + }, + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Cocktail Bars", + "alias": "cocktailbars" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3355 South Las Vegas Blvd\nSte 106\nLas Vegas, NV 89109" + } + }, + { + "id": "-1m9o3vGRA8IBPNvNqKLmA", + "name": "Bavette's Steakhouse & Bar", + "price": "$$$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/EU9ecdF4QA269NoDYyfHIw/o.jpg" + ], + "reviews": [ + { + "id": "NDiYCISmBsPBFLnI_OVW3w", + "rating": 5, + "user": { + "id": "4HV_2n-EOEthiv-jmCxJSQ", + "image_url": null, + "name": "Tammy F." + } + }, + { + "id": "rg0OovE_wwhE1zgEIm3znQ", + "rating": 5, + "user": { + "id": "gEn-EfHvKvazcLESy8u_wg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/RJqZxXHxdw2JW5O_5miBoQ/o.jpg", + "name": "Corey C." + } + }, + { + "id": "LrkIgX8AjGEogbycrCeZkQ", + "rating": 2, + "user": { + "id": "eC96DlMK61qDz9btY1jDMg", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/9XXLItTAeWHYUOBJalFDLw/o.jpg", + "name": "Rick R." + } + } + ], + "categories": [ + { + "title": "Steakhouses", + "alias": "steak" + }, + { + "title": "Bars", + "alias": "bars" + }, + { + "title": "New American", + "alias": "newamerican" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "3770 Las Vegas Blvd S\nLas Vegas, NV 89109" + } + }, + { + "id": "7sb2FYLS2sejZKxRYF9mtg", + "name": "Sakana", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media3.fl.yelpcdn.com/bphoto/NmJ4Mgc8uKMCC6xCKivaiA/o.jpg" + ], + "reviews": [ + { + "id": "tGVt9NJkHavNOQlDxERbQw", + "rating": 5, + "user": { + "id": "udTGkgCBE7SM_DvIuxT2SA", + "image_url": null, + "name": "Andrew Y." + } + }, + { + "id": "do79yG90A0Rt7tl0c5ixsA", + "rating": 5, + "user": { + "id": "T5jeU5bR4j5ljCWwMgDwiA", + "image_url": "https://s3-media2.fl.yelpcdn.com/photo/QHzer_tvY2HcTgarqc23DA/o.jpg", + "name": "Raymond R." + } + }, + { + "id": "X3dQ5XnsAQ8qFdy5OWg1WA", + "rating": 5, + "user": { + "id": "oRoBMXam0EdSA2Wtc4kmOQ", + "image_url": "https://s3-media1.fl.yelpcdn.com/photo/LuvBvcoYFLIOT67Lbe4i6w/o.jpg", + "name": "Julia L." + } + } + ], + "categories": [ + { + "title": "Japanese", + "alias": "japanese" + }, + { + "title": "Sushi Bars", + "alias": "sushi" + }, + { + "title": "Bars", + "alias": "bars" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "3949 S Maryland Pkwy\nLas Vegas, NV 89119" + } + }, + { + "id": "_Ad2ZKhUl-krJFpaZ1FI8g", + "name": "Nabe Hotpot", + "price": "$$", + "rating": 4.3, + "photos": [ + "https://s3-media2.fl.yelpcdn.com/bphoto/942m9pXmKL8Hdh2VDbbbwA/o.jpg" + ], + "reviews": [ + { + "id": "OuObIP40RIJ9FN3eWcHnew", + "rating": 5, + "user": { + "id": "qS-PHP8sywzYWTMhMcK4lA", + "image_url": null, + "name": "Sasa B." + } + }, + { + "id": "Nkbqvwb5M47Z4unVebLMKw", + "rating": 5, + "user": { + "id": "qvzEhAdcRKistX6kxQhAVA", + "image_url": null, + "name": "Sarah Mae P." + } + }, + { + "id": "eFzQJGOfOgrR_m89EdAIBA", + "rating": 5, + "user": { + "id": "3Srg9-qwOtUxY9eyJI4-yg", + "image_url": null, + "name": "Nikileen B." + } + } + ], + "categories": [ + { + "title": "Hot Pot", + "alias": "hotpot" + }, + { + "title": "Buffets", + "alias": "buffets" + }, + { + "title": "Asian Fusion", + "alias": "asianfusion" + } + ], + "hours": [ + { + "is_open_now": true + } + ], + "location": { + "formatted_address": "4545 Spring Mountain Rd\nSte106\nLas Vegas, NV 89103" + } + }, + { + "id": "3kdSl5mo9dWC4clrQjEDGg", + "name": "Egg & I", + "price": "$$", + "rating": 4.5, + "photos": [ + "https://s3-media1.fl.yelpcdn.com/bphoto/z4rdxoc6xaM4dmdPovPBDg/o.jpg" + ], + "reviews": [ + { + "id": "zLTki3FhRtLazq8lITHsPw", + "rating": 5, + "user": { + "id": "rfc-7fqA9cOElpRh1LRLcw", + "image_url": null, + "name": "Steven C." + } + }, + { + "id": "ryqGTnDkY5U0ZuFdg1S1fQ", + "rating": 5, + "user": { + "id": "SlUAUp7am-X8RhfZ_HWf_w", + "image_url": null, + "name": "Corey C." + } + }, + { + "id": "2gnSQ6VigIFCXhIjcUR3Kg", + "rating": 5, + "user": { + "id": "3LOAOpov-lnr7Ock1n4m6w", + "image_url": null, + "name": "Ted S." + } + } + ], + "categories": [ + { + "title": "Breakfast & Brunch", + "alias": "breakfast_brunch" + }, + { + "title": "Burgers", + "alias": "burgers" + }, + { + "title": "American", + "alias": "tradamerican" + } + ], + "hours": [ + { + "is_open_now": false + } + ], + "location": { + "formatted_address": "4533 W Sahara Ave\nSte 5\nLas Vegas, NV 89102" + } + } + ] + } + } +} \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/home_screen.dart b/lib/features/home_page/presenter/page/home_screen.dart index 1144f5c..e8e306a 100644 --- a/lib/features/home_page/presenter/page/home_screen.dart +++ b/lib/features/home_page/presenter/page/home_screen.dart @@ -49,43 +49,43 @@ class _Body extends StatelessWidget { Widget build(BuildContext context) { return DefaultTabController( length: 2, - child: SingleChildScrollView( - physics: BouncingScrollPhysics(), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Material( - elevation: 6.0, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10.0), - topRight: Radius.circular(10.0), - ), - child: TabBar( - indicator: UnderlineTabIndicator( - borderSide: BorderSide( - color: Colors.black, - width: 2.0, - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Material( + elevation: 6.0, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + child: TabBar( + indicator: UnderlineTabIndicator( + borderSide: BorderSide( + color: Colors.black, + width: 2.0, ), - labelColor: Colors.black, - unselectedLabelColor: Colors.grey, - tabs: [ - Tab(text: 'All Restaurants'), - Tab(text: 'My Favorites'), - ], ), + labelColor: Colors.black, + unselectedLabelColor: Colors.grey, + tabs: [ + Tab(text: 'All Restaurants'), + Tab(text: 'My Favorites'), + ], ), - BlocBuilder( - builder: (context, state) { - if (state is HomeLoadingState) { - return const HomeLoadingSkeleton(); - } else { - return const TabViews(); - } - }, - ), - ], - ), + ), + BlocBuilder( + builder: (context, state) { + if (state is HomeLoadingState) { + return const HomeLoadingSkeleton(); + } + if (state is HomeDataLoadedState) { + return TabViews(restaurantList: state.restaurantList); + } else { + return const SizedBox(); + } + }, + ), + ], ), ); } diff --git a/lib/features/home_page/presenter/page/widgets/tab_views.dart b/lib/features/home_page/presenter/page/widgets/tab_views.dart index 3ac2d15..778bed6 100644 --- a/lib/features/home_page/presenter/page/widgets/tab_views.dart +++ b/lib/features/home_page/presenter/page/widgets/tab_views.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; import 'package:restaurant_tour/features/home_page/presenter/page/widgets/all_restaurants_tab.dart'; import 'package:restaurant_tour/features/home_page/presenter/page/widgets/my_favorites_tab.dart'; - class TabViews extends StatelessWidget { - const TabViews({super.key}); + const TabViews({ + super.key, + required this.restaurantList, + }); + + final List restaurantList; @override Widget build(BuildContext context) { @@ -23,4 +28,4 @@ class TabViews extends StatelessWidget { ], ); } -} \ No newline at end of file +} From 269104a704678d8feadbf121454e0b4511211283 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 10:23:40 -0400 Subject: [PATCH 13/34] feat: card restaurant, implementation dynamic display of data --- .../page/widgets/all_restaurants_tab.dart | 29 ++++++++++--------- .../page/widgets/card_restaurant.dart | 21 ++++++++++++++ .../page/widgets/home_loading_skeleton.dart | 16 +++++----- .../presenter/page/widgets/tab_views.dart | 20 +++++-------- 4 files changed, 52 insertions(+), 34 deletions(-) create mode 100644 lib/features/home_page/presenter/page/widgets/card_restaurant.dart diff --git a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart index e8e94d2..5226b09 100644 --- a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart +++ b/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart @@ -1,23 +1,24 @@ import 'package:flutter/material.dart'; -import 'package:restaurant_tour/core/constants.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/widgets/restaurant_card_skeleton.dart'; -import 'package:restaurant_tour/shared/rt_skeleton.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/widgets/card_restaurant.dart'; class AllRestaurantsTab extends StatelessWidget { - const AllRestaurantsTab({super.key}); + const AllRestaurantsTab({ + super.key, + required this.restaurantList, + }); + + final List restaurantList; @override Widget build(BuildContext context) { - return const Center( - child: Padding( - padding: EdgeInsets.only(top: kPaddingTopTabBar), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text('All Restaurants') - ], - ), - ), + return ListView.builder( + padding: const EdgeInsets.only(top: 8.0), + itemCount: restaurantList.length, + itemBuilder: (context, index) { + final restaurant = restaurantList[index]; + return CardRestaurant(restaurant: restaurant); + }, ); } } diff --git a/lib/features/home_page/presenter/page/widgets/card_restaurant.dart b/lib/features/home_page/presenter/page/widgets/card_restaurant.dart new file mode 100644 index 0000000..cc3356f --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/card_restaurant.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; + + +class CardRestaurant extends StatelessWidget { + const CardRestaurant({ + super.key, + required this.restaurant, + }); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return Card( + child: ListTile( + title: Text(restaurant.name ?? 'No name provided'), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart b/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart index 8809a70..1e17a77 100644 --- a/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart +++ b/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart @@ -6,13 +6,15 @@ class HomeLoadingSkeleton extends StatelessWidget { @override Widget build(BuildContext context) { - return const Column( - children: [ - RestaurantCardSkeleton(), - RestaurantCardSkeleton(), - RestaurantCardSkeleton(), - RestaurantCardSkeleton(), - ], + const int itemCount = 6; + + return Expanded( + child: ListView.builder( + itemCount: itemCount, + itemBuilder: (BuildContext context, int index) { + return const RestaurantCardSkeleton(); + }, + ), ); } } diff --git a/lib/features/home_page/presenter/page/widgets/tab_views.dart b/lib/features/home_page/presenter/page/widgets/tab_views.dart index 778bed6..f1f9c7f 100644 --- a/lib/features/home_page/presenter/page/widgets/tab_views.dart +++ b/lib/features/home_page/presenter/page/widgets/tab_views.dart @@ -13,19 +13,13 @@ class TabViews extends StatelessWidget { @override Widget build(BuildContext context) { - return const Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 200, - child: TabBarView( - children: [ - AllRestaurantsTab(), - MyFavoritesTab(), - ], - ), - ), - ], + return Flexible( + child: TabBarView( + children: [ + AllRestaurantsTab(restaurantList: restaurantList), + MyFavoritesTab(), + ], + ), ); } } From f746f958a06d3c6fc3a76de10b6b9c42c8010a93 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 10:29:21 -0400 Subject: [PATCH 14/34] feat: added fake data into yelp repository --- lib/repositories/yelp_repository.dart | 47 ++++++++++++++++++--------- pubspec.yaml | 2 +- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index ae9db62..e6f283e 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/repositories/yelp_repository.dart @@ -1,4 +1,7 @@ +import 'dart:convert'; + import 'package:dio/dio.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:oxidized/oxidized.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; @@ -18,25 +21,37 @@ class YelpRepository { ); } - Future> getRestaurants({int offset = 0}) async { + Future> getRestaurants( + {int offset = 0}) async { try { - final response = await dio.post>( - '/v3/graphql', - data: _getQuery(offset), - ); - final result = RestaurantQueryResult.fromJson(response.data!['data']['search']); + final String jsonString = + await rootBundle.loadString('assets/restaurants.json'); + final Map jsonResponse = json.decode(jsonString); + // final response = await dio.post>( + // '/v3/graphql', + // data: _getQuery(offset), + // ); + // final result = RestaurantQueryResult.fromJson(response.data!['data']['search']); + final result = + RestaurantQueryResult.fromJson(jsonResponse['data']['search']); return Ok(result); } catch (e) { - if(e is DioException) { - return Err(e); - } else { - return Err( - DioException( - requestOptions: RequestOptions(path: '/v3/graphql'), - error: e, - ), - ); - } + return Err( + DioException( + requestOptions: RequestOptions(path: 'path'), + error: e.toString(), + ), + ); + // if(e is DioException) { + // return Err(e); + // } else { + // return Err( + // DioException( + // requestOptions: RequestOptions(path: '/v3/graphql'), + // error: e, + // ), + // ); + // } } } diff --git a/pubspec.yaml b/pubspec.yaml index e650778..eabd052 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,7 +35,7 @@ flutter: uses-material-design: true assets: - .env - - assets/images/ + - assets/ fonts: - family: Lora fonts: From 20737488b15ed65afe0f3453b07860e704320a0d Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 10:49:29 -0400 Subject: [PATCH 15/34] feat: added card restaurant, widget stars rate, status widget --- .../page/widgets/card_restaurant.dart | 91 ++++++++++++++++++- .../presenter/page/widgets/rate_stars.dart | 31 +++++++ .../page/widgets/status_indicator.dart | 31 +++++++ pubspec.yaml | 1 + 4 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 lib/features/home_page/presenter/page/widgets/rate_stars.dart create mode 100644 lib/features/home_page/presenter/page/widgets/status_indicator.dart diff --git a/lib/features/home_page/presenter/page/widgets/card_restaurant.dart b/lib/features/home_page/presenter/page/widgets/card_restaurant.dart index cc3356f..7b62e2d 100644 --- a/lib/features/home_page/presenter/page/widgets/card_restaurant.dart +++ b/lib/features/home_page/presenter/page/widgets/card_restaurant.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/widgets/rate_stars.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/widgets/status_indicator.dart'; class CardRestaurant extends StatelessWidget { @@ -12,9 +14,92 @@ class CardRestaurant extends StatelessWidget { @override Widget build(BuildContext context) { - return Card( - child: ListTile( - title: Text(restaurant.name ?? 'No name provided'), + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 8, + ), + child: Card( + child: IntrinsicHeight( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: 90, + height: 90, + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Image.network( + restaurant.photos!.first, + fit: BoxFit.cover, + ), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + restaurant.name ?? 'No name provided', + maxLines: 2, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.ellipsis, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + children: [ + Text( + restaurant.price ?? '', + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 4.0), + child: Text( + restaurant.categories?.first.title ?? '', + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + const Spacer(), + Row( + children: [ + RateStars( + rate: restaurant.rating ?? 0.0, + starSize: 20.0, + color: Colors.amber, + ), + const Spacer(), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: restaurant.isOpen + ? const StatusIndicator( + text: "Open Now", + color: Colors.green, + ) + : const StatusIndicator( + text: "Closed", + color: Colors.red, + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), ), ); } diff --git a/lib/features/home_page/presenter/page/widgets/rate_stars.dart b/lib/features/home_page/presenter/page/widgets/rate_stars.dart new file mode 100644 index 0000000..9e6e1b4 --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/rate_stars.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class RateStars extends StatelessWidget { + final double rate; + final double starSize; + final Color color; + + const RateStars({ + Key? key, + required this.rate, + this.starSize = 20.0, + this.color = Colors.amber, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + List stars = []; + int wholeStars = rate.floor(); + bool isHalfStar = rate - wholeStars >= 0.5; + + for (int i = 0; i < wholeStars; i++) { + stars.add(Icon(Icons.star, size: starSize, color: color)); + } + + if (isHalfStar) { + stars.add(Icon(Icons.star_half, size: starSize, color: color)); + } + + return Row(children: stars); + } +} \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/widgets/status_indicator.dart b/lib/features/home_page/presenter/page/widgets/status_indicator.dart new file mode 100644 index 0000000..e93557e --- /dev/null +++ b/lib/features/home_page/presenter/page/widgets/status_indicator.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +class StatusIndicator extends StatelessWidget { + const StatusIndicator({super.key, required this.text, required this.color}); + + final String text; + final MaterialColor color; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text( + text, + style: const TextStyle( + fontStyle: FontStyle.italic, + ), + ), + const SizedBox(width: 4), + Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index eabd052..ce48bb0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,7 @@ flutter: uses-material-design: true assets: - .env + - assets/images/ - assets/ fonts: - family: Lora From 2ae0e9da55a02ec02bba3e33a0db9d10724a24c7 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 11:10:25 -0400 Subject: [PATCH 16/34] feat: restaurant screen, bloc - added hero animation in card restaurant - Creation of feature restaurant screen | detail - Integration with card restaurant in home | hero animation - creation of logic of restaurant screen feature with flutter bloc --- lib/core/navigation/route_navigator.dart | 10 ++ .../page/widgets/card_restaurant.dart | 152 ++++++++++-------- .../presenter/bloc/restaurant_bloc.dart | 13 ++ .../presenter/bloc/restaurant_event.dart | 5 + .../presenter/bloc/restaurant_state.dart | 10 ++ .../presenter/page/restaurant_screen.dart | 77 +++++++++ .../page/widgets/custom_app_bar.dart | 29 ++++ .../presenter/page/widgets/raiting_area.dart | 51 ++++++ .../page/widgets/restaurant_details_area.dart | 76 +++++++++ .../presenter/page/widgets/reviews_area.dart | 45 ++++++ 10 files changed, 397 insertions(+), 71 deletions(-) create mode 100644 lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart create mode 100644 lib/features/restaurant_page/presenter/bloc/restaurant_event.dart create mode 100644 lib/features/restaurant_page/presenter/bloc/restaurant_state.dart create mode 100644 lib/features/restaurant_page/presenter/page/restaurant_screen.dart create mode 100644 lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart create mode 100644 lib/features/restaurant_page/presenter/page/widgets/raiting_area.dart create mode 100644 lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart create mode 100644 lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart diff --git a/lib/core/navigation/route_navigator.dart b/lib/core/navigation/route_navigator.dart index a7e4fdf..f8fa1ba 100644 --- a/lib/core/navigation/route_navigator.dart +++ b/lib/core/navigation/route_navigator.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; import 'package:restaurant_tour/features/home_page/presenter/page/home_screen.dart'; +import 'package:restaurant_tour/features/restaurant_page/presenter/page/restaurant_screen.dart'; import 'package:restaurant_tour/features/splash_screen/presenter/splash_screen.dart'; final GoRouter router = GoRouter( @@ -18,5 +20,13 @@ final GoRouter router = GoRouter( return const HomeScreen(); }, ), + GoRoute( + path: '/restaurant-screen', + name: 'restaurant-screen', + builder: (BuildContext context, GoRouterState state) { + final Restaurant restaurant = state.extra as Restaurant; + return RestaurantScreen(restaurant: restaurant); + }, + ), ], ); \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/widgets/card_restaurant.dart b/lib/features/home_page/presenter/page/widgets/card_restaurant.dart index 7b62e2d..8a53bd0 100644 --- a/lib/features/home_page/presenter/page/widgets/card_restaurant.dart +++ b/lib/features/home_page/presenter/page/widgets/card_restaurant.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; import 'package:restaurant_tour/features/home_page/presenter/page/widgets/rate_stars.dart'; import 'package:restaurant_tour/features/home_page/presenter/page/widgets/status_indicator.dart'; @@ -14,90 +15,99 @@ class CardRestaurant extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 4.0, - horizontal: 8, - ), - child: Card( - child: IntrinsicHeight( - child: Row( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: SizedBox( - width: 90, - height: 90, - child: ClipRRect( - borderRadius: BorderRadius.circular(5), - child: Image.network( - restaurant.photos!.first, - fit: BoxFit.cover, + return InkWell( + onTap: () { + context.pushNamed('restaurant-screen', extra: restaurant); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0, + horizontal: 8, + ), + child: Card( + child: IntrinsicHeight( + child: Row( + children: [ + Hero( + tag: 'restaurant-image-${restaurant.id}', + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SizedBox( + width: 90, + height: 90, + child: ClipRRect( + borderRadius: BorderRadius.circular(5), + child: Image.network( + restaurant.photos!.first, + fit: BoxFit.cover, + ), + ), ), ), ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - restaurant.name ?? 'No name provided', - maxLines: 2, - style: Theme.of(context).textTheme.titleMedium, - overflow: TextOverflow.ellipsis, - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Row( + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + restaurant.name ?? 'No name provided', + maxLines: 2, + style: Theme.of(context).textTheme.titleMedium, + overflow: TextOverflow.ellipsis, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + children: [ + Text( + restaurant.price ?? '', + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 4.0), + child: Text( + restaurant.categories?.first.title ?? '', + style: Theme.of(context).textTheme.bodyMedium, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + const Spacer(), + Row( children: [ - Text( - restaurant.price ?? '', - style: Theme.of(context).textTheme.bodyMedium, - overflow: TextOverflow.ellipsis, + RateStars( + rate: restaurant.rating ?? 0.0, + starSize: 20.0, + color: Colors.amber, ), + const Spacer(), Padding( padding: - const EdgeInsets.symmetric(horizontal: 4.0), - child: Text( - restaurant.categories?.first.title ?? '', - style: Theme.of(context).textTheme.bodyMedium, - overflow: TextOverflow.ellipsis, + const EdgeInsets.symmetric(vertical: 8.0), + child: restaurant.isOpen + ? const StatusIndicator( + text: "Open Now", + color: Colors.green, + ) + : const StatusIndicator( + text: "Closed", + color: Colors.red, ), ), ], ), - ), - const Spacer(), - Row( - children: [ - RateStars( - rate: restaurant.rating ?? 0.0, - starSize: 20.0, - color: Colors.amber, - ), - const Spacer(), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: restaurant.isOpen - ? const StatusIndicator( - text: "Open Now", - color: Colors.green, - ) - : const StatusIndicator( - text: "Closed", - color: Colors.red, - ), - ), - ], - ), - ], + ], + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart new file mode 100644 index 0000000..062f291 --- /dev/null +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart @@ -0,0 +1,13 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; + +part 'restaurant_event.dart'; +part 'restaurant_state.dart'; + +class RestaurantBloc extends Bloc { + RestaurantBloc() : super(RestaurantInitial()) { + on((event, emit) { + // TODO: implement event handler + }); + } +} diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart new file mode 100644 index 0000000..5d5fca8 --- /dev/null +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart @@ -0,0 +1,5 @@ +part of 'restaurant_bloc.dart'; + +sealed class RestaurantEvent extends Equatable { + const RestaurantEvent(); +} diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart new file mode 100644 index 0000000..c7ed721 --- /dev/null +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart @@ -0,0 +1,10 @@ +part of 'restaurant_bloc.dart'; + +sealed class RestaurantState extends Equatable { + const RestaurantState(); +} + +final class RestaurantInitial extends RestaurantState { + @override + List get props => []; +} diff --git a/lib/features/restaurant_page/presenter/page/restaurant_screen.dart b/lib/features/restaurant_page/presenter/page/restaurant_screen.dart new file mode 100644 index 0000000..6d2b504 --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/restaurant_screen.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/restaurant_page/presenter/bloc/restaurant_bloc.dart'; +import 'package:restaurant_tour/features/restaurant_page/presenter/page/widgets/raiting_area.dart'; +import 'package:restaurant_tour/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart'; +import 'package:restaurant_tour/features/restaurant_page/presenter/page/widgets/reviews_area.dart'; + +import 'widgets/custom_app_bar.dart'; + + +class RestaurantScreen extends StatelessWidget { + const RestaurantScreen({super.key, required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => RestaurantBloc(), + child: _Page(restaurant: restaurant), + ); + } +} + +class _Page extends StatelessWidget { + const _Page({required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CustomAppBar(title: restaurant.name), + body: _Body(restaurant: restaurant), + ); + } +} + +class _Body extends StatelessWidget { + const _Body({required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + final width = MediaQuery.sizeOf(context).width; + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + tag: 'restaurant-image-${restaurant.id}', + child: Image.network( + restaurant.photos!.first, + fit: BoxFit.cover, + width: width, + height: width, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16), + child: Column( + children: [ + RestaurantDetailsArea(restaurant: restaurant), + RatingArea( + rating: restaurant.rating.toString(), + ), + ReviewsArea(reviews: restaurant.reviews), + ], + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart b/lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart new file mode 100644 index 0000000..79728de --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + const CustomAppBar({super.key, this.title}); + + final String? title; + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); + + @override + Widget build(BuildContext context) { + return AppBar( + leading: BackButton( + onPressed: () { + context.pop(context); + }, + ), + title: Text(title!), + actions: [ + IconButton( + icon: const Icon(Icons.favorite_border), + onPressed: () {}, + ) + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/page/widgets/raiting_area.dart b/lib/features/restaurant_page/presenter/page/widgets/raiting_area.dart new file mode 100644 index 0000000..16b6522 --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/widgets/raiting_area.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +class RatingArea extends StatelessWidget { + const RatingArea({super.key, required this.rating}); + + final String rating; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.shade300, + width: 1.0, + ), + ), + ), + child: Row( + children: [ + Column( + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Text('Overall Rating'), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + rating, + style: const TextStyle( + fontSize: 30, + fontWeight: FontWeight.bold, + ), + ), + const Icon( + Icons.star, + color: Colors.amber, + size: 20, + ), + ], + ), + ], + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart b/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart new file mode 100644 index 0000000..b2e2052 --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/widgets/status_indicator.dart'; + + +class RestaurantDetailsArea extends StatelessWidget { + const RestaurantDetailsArea({super.key, required this.restaurant}); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 16.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.shade300, + width: 1.0, + ), + ), + ), + child: Row( + children: [ + Text(restaurant.price.toString() + ', '), + Text( + restaurant.categories?.first.title ?? '', + ), + const Spacer(), + restaurant.isOpen + ? const StatusIndicator( + text: "Open Now", + color: Colors.green, + ) + : const StatusIndicator( + text: "Closed", + color: Colors.red, + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(vertical: 16.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.shade300, + width: 1.0, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Address'), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + restaurant.location?.formattedAddress ?? '', + ), + ), + ], + ), + ], + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart new file mode 100644 index 0000000..9a4b60c --- /dev/null +++ b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; + +class ReviewsArea extends StatelessWidget { + const ReviewsArea({ + super.key, + required this.reviews, + }); + + final List? reviews; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.grey.shade300, + width: 1.0, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Reviews ${reviews?.length}'), + Text('${reviews?.first.rating}'), + const Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Text( + 'restaurant.location?.formattedAddress ?? '',' + ), + ), + ], + ), + ], + ), + ); + } +} \ No newline at end of file From 089abdbdf64e7cc516e61ffaeef4ba4fc3d41be3 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 11:15:51 -0400 Subject: [PATCH 17/34] feat: restaurant screen detail - added reviews widget --- .../page/widgets/my_favorites_tab.dart | 1 - .../presenter/page/widgets/rate_stars.dart | 8 +- .../presenter/page/widgets/reviews_area.dart | 88 +++++++++++++------ 3 files changed, 63 insertions(+), 34 deletions(-) diff --git a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart index 9d98b9b..89e5425 100644 --- a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart +++ b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/constants.dart'; -import 'package:restaurant_tour/shared/rt_skeleton.dart'; class MyFavoritesTab extends StatelessWidget { diff --git a/lib/features/home_page/presenter/page/widgets/rate_stars.dart b/lib/features/home_page/presenter/page/widgets/rate_stars.dart index 9e6e1b4..3fcdfda 100644 --- a/lib/features/home_page/presenter/page/widgets/rate_stars.dart +++ b/lib/features/home_page/presenter/page/widgets/rate_stars.dart @@ -1,10 +1,6 @@ import 'package:flutter/material.dart'; class RateStars extends StatelessWidget { - final double rate; - final double starSize; - final Color color; - const RateStars({ Key? key, required this.rate, @@ -12,6 +8,10 @@ class RateStars extends StatelessWidget { this.color = Colors.amber, }) : super(key: key); + final double rate; + final double starSize; + final Color color; + @override Widget build(BuildContext context) { List stars = []; diff --git a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart index 9a4b60c..8574155 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/widgets/rate_stars.dart'; class ReviewsArea extends StatelessWidget { const ReviewsArea({ @@ -11,35 +12,64 @@ class ReviewsArea extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 16.0), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Colors.grey.shade300, - width: 1.0, - ), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Reviews ${reviews?.length}'), - Text('${reviews?.first.rating}'), - const Padding( - padding: EdgeInsets.symmetric(vertical: 16.0), - child: Text( - 'restaurant.location?.formattedAddress ?? '',' + return Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text('Reviews ${reviews?.length ?? 0}'), + ), + if (reviews != null) + for (Review review in reviews!) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: RateStars(rate: review.rating?.toDouble() ?? 0.0), + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ClipOval( + child: Image.network( + review.user?.imageUrl ?? + 'https://fakeimg.pl/600x400', + width: 40, + height: 40, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Image.network( + 'https://fakeimg.pl/600x400', + width: 40, + height: 40, + fit: BoxFit.cover, + ); + }, + ), + ), + ), + Text(review.user?.name ?? ''), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Container( + color: Colors.grey.shade300, + height: 1, + width: MediaQuery.sizeOf(context).width * 0.9, + ), + ), + ], ), - ), - ], - ), - ], - ), + ], + ), + ], ); } -} \ No newline at end of file +} From e1248b5fa16dbfd9444ed557037e674cbdd01ed3 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 11:41:59 -0400 Subject: [PATCH 18/34] feat: added hive helper for favorite items - added appInit class --- ios/Flutter/Debug.xcconfig | 1 + ios/Podfile | 44 ++++++++++++++ lib/core/app_init.dart | 10 ++++ lib/core/helpers/hive_helper.dart | 49 ++++++++++++++++ lib/main.dart | 2 +- pubspec.lock | 98 ++++++++++++++++++++++++++++++- pubspec.yaml | 3 + 7 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 ios/Podfile create mode 100644 lib/core/app_init.dart create mode 100644 lib/core/helpers/hive_helper.dart diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/core/app_init.dart b/lib/core/app_init.dart new file mode 100644 index 0000000..b283ad5 --- /dev/null +++ b/lib/core/app_init.dart @@ -0,0 +1,10 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; + + +class AppInit { + static Future initializeApp() async { + await dotenv.load(fileName: ".env"); + await HiveHelper().init(); + } +} \ No newline at end of file diff --git a/lib/core/helpers/hive_helper.dart b/lib/core/helpers/hive_helper.dart new file mode 100644 index 0000000..8e8fa56 --- /dev/null +++ b/lib/core/helpers/hive_helper.dart @@ -0,0 +1,49 @@ +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:restaurant_tour/core/models/restaurant.dart'; + + +class HiveHelper { + static final HiveHelper _singleton = HiveHelper._internal(); + late Box box; + + factory HiveHelper() { + return _singleton; + } + + HiveHelper._internal(); + + Future init() async { + final appDocumentDir = + await path_provider.getApplicationDocumentsDirectory(); + Hive.init(appDocumentDir.path); + box = await Hive.openBox('favorites'); + } + + dynamic get(String key) { + return box.get(key); + } + + Future put(String key, dynamic value) async { + await box.put(key, value); + } + + Future delete(String key) async { + await box.delete(key); + } + + List getAllRestaurants() { + return box.values.map((e) => Restaurant.fromJson(e)).toList(); + } + + List getAllFavoriteIds() { + List values = box.values.toList(); + List favoriteIds = []; + for (var value in values) { + if (value is Map) { + favoriteIds.add(value['id'],); + } + } + return favoriteIds; + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 7ad0404..4e019b4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,7 +4,7 @@ import 'package:restaurant_tour/core/navigation/route_navigator.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - await dotenv.load(fileName: ".env"); + AppInit.initializeApp(); runApp( const RestaurantTour(), ); diff --git a/pubspec.lock b/pubspec.lock index 76d1065..f026024 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -209,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" file: dependency: transitive description: @@ -296,6 +304,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.dev" + source: hosted + version: "1.1.0" http: dependency: "direct main" description: @@ -456,6 +480,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + url: "https://pub.dev" + source: hosted + version: "2.1.4" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + url: "https://pub.dev" + source: hosted + version: "2.2.10" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: @@ -653,6 +741,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" yaml: dependency: transitive description: @@ -663,4 +759,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.6" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index ce48bb0..6bcebea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -18,9 +18,12 @@ dependencies: flutter_bloc: ^8.1.6 flutter_dotenv: ^5.1.0 go_router: ^14.2.7 + hive: ^2.2.3 + hive_flutter: ^1.1.0 http: ^1.2.2 json_annotation: ^4.9.0 oxidized: ^6.2.0 + path_provider: ^2.1.4 shimmer: ^3.0.0 dev_dependencies: From b887378c8500523635fab2d3c441282247116929 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 11:56:29 -0400 Subject: [PATCH 19/34] feat: added hive local favorite and remove functions --- ios/Flutter/Release.xcconfig | 1 + lib/core/helpers/hive_helper.dart | 34 ++++-------- .../presenter/bloc/restaurant_bloc.dart | 55 +++++++++++++++++-- .../presenter/bloc/restaurant_event.dart | 27 +++++++++ .../presenter/bloc/restaurant_state.dart | 41 ++++++++++++++ .../presenter/page/restaurant_screen.dart | 19 +++++-- .../page/widgets/custom_app_bar.dart | 51 ++++++++++++++--- lib/main.dart | 2 +- 8 files changed, 188 insertions(+), 42 deletions(-) diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/lib/core/helpers/hive_helper.dart b/lib/core/helpers/hive_helper.dart index 8e8fa56..58e2d17 100644 --- a/lib/core/helpers/hive_helper.dart +++ b/lib/core/helpers/hive_helper.dart @@ -1,6 +1,5 @@ import 'package:hive/hive.dart'; import 'package:path_provider/path_provider.dart' as path_provider; -import 'package:restaurant_tour/core/models/restaurant.dart'; class HiveHelper { @@ -20,30 +19,21 @@ class HiveHelper { box = await Hive.openBox('favorites'); } - dynamic get(String key) { - return box.get(key); - } - - Future put(String key, dynamic value) async { - await box.put(key, value); - } - - Future delete(String key) async { - await box.delete(key); + Future addFavorite(String restaurantId) async { + List favorites = getAllFavoriteIds(); + if (!favorites.contains(restaurantId)) { + favorites.add(restaurantId); + await box.put('favoriteIds', favorites); + } } - List getAllRestaurants() { - return box.values.map((e) => Restaurant.fromJson(e)).toList(); + Future removeFavorite(String restaurantId) async { + List favorites = getAllFavoriteIds(); + favorites.remove(restaurantId); + await box.put('favoriteIds', favorites); } List getAllFavoriteIds() { - List values = box.values.toList(); - List favoriteIds = []; - for (var value in values) { - if (value is Map) { - favoriteIds.add(value['id'],); - } - } - return favoriteIds; + return box.get('favoriteIds', defaultValue: [])!.cast(); } -} \ No newline at end of file + } \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart index 062f291..31d58fc 100644 --- a/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart @@ -1,13 +1,58 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; part 'restaurant_event.dart'; part 'restaurant_state.dart'; class RestaurantBloc extends Bloc { - RestaurantBloc() : super(RestaurantInitial()) { - on((event, emit) { - // TODO: implement event handler - }); + RestaurantBloc({required this.hiveHelper}) : super(RestaurantInitial()) { + on(_onCheckFavoriteEvent); + on(_onAddFavoriteEvent); + on(_onRemoveFavoriteEvent); } -} + + final HiveHelper hiveHelper; + + Future _onCheckFavoriteEvent( + CheckFavoriteEvent event, + Emitter emit, + ) async { + emit(const LoadingState()); + try { + List favoriteIds = hiveHelper.getAllFavoriteIds(); + bool isFavorite = favoriteIds.contains(event.restaurant.id); + + emit(VerifiedState(isFavorite: isFavorite)); + } catch (e) { + emit(ErrorState(message: e.toString())); + } + } + + Future _onAddFavoriteEvent( + AddFavoriteEvent event, + Emitter emit, + ) async { + emit(const LoadingState()); + try { + await hiveHelper.addFavorite(event.restaurantId); + emit(const VerifiedState(isFavorite: true)); + } catch (e) { + emit(FavoriteOperationError(message: e.toString())); + } + } + + Future _onRemoveFavoriteEvent( + RemoveFavoriteEvent event, + Emitter emit, + ) async { + emit(const LoadingState()); + try { + await hiveHelper.removeFavorite(event.restaurantId); + emit(const VerifiedState(isFavorite: false)); + } catch (e) { + emit(FavoriteOperationError(message: e.toString())); + } + } +} \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart index 5d5fca8..b74deee 100644 --- a/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart @@ -3,3 +3,30 @@ part of 'restaurant_bloc.dart'; sealed class RestaurantEvent extends Equatable { const RestaurantEvent(); } + +class CheckFavoriteEvent extends RestaurantEvent { + const CheckFavoriteEvent({required this.restaurant}); + + final Restaurant restaurant; + + @override + List get props => [restaurant]; +} + +class AddFavoriteEvent extends RestaurantEvent { + const AddFavoriteEvent({required this.restaurantId}); + + final String restaurantId; + + @override + List get props => [restaurantId]; +} + +class RemoveFavoriteEvent extends RestaurantEvent { + const RemoveFavoriteEvent({required this.restaurantId}); + + final String restaurantId; + + @override + List get props => [restaurantId]; +} \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart index c7ed721..ba4df9a 100644 --- a/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart @@ -8,3 +8,44 @@ final class RestaurantInitial extends RestaurantState { @override List get props => []; } + +class LoadingState extends RestaurantState { + const LoadingState(); + + @override + List get props => []; +} + +class VerifiedState extends RestaurantState { + const VerifiedState({required this.isFavorite}); + + final bool isFavorite; + + @override + List get props => [isFavorite]; +} + +class ErrorState extends RestaurantState { + const ErrorState({required this.message}); + + final String message; + + @override + List get props => []; +} + +class FavoriteOperationSuccess extends RestaurantState { + const FavoriteOperationSuccess(); + + @override + List get props => []; +} + +class FavoriteOperationError extends RestaurantState { + const FavoriteOperationError({required this.message}); + + final String message; + + @override + List get props => [message]; +} \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/page/restaurant_screen.dart b/lib/features/restaurant_page/presenter/page/restaurant_screen.dart index 6d2b504..955a205 100644 --- a/lib/features/restaurant_page/presenter/page/restaurant_screen.dart +++ b/lib/features/restaurant_page/presenter/page/restaurant_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; import 'package:restaurant_tour/features/restaurant_page/presenter/bloc/restaurant_bloc.dart'; import 'package:restaurant_tour/features/restaurant_page/presenter/page/widgets/raiting_area.dart'; @@ -8,7 +9,6 @@ import 'package:restaurant_tour/features/restaurant_page/presenter/page/widgets/ import 'widgets/custom_app_bar.dart'; - class RestaurantScreen extends StatelessWidget { const RestaurantScreen({super.key, required this.restaurant}); @@ -17,8 +17,17 @@ class RestaurantScreen extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => RestaurantBloc(), - child: _Page(restaurant: restaurant), + create: (context) => RestaurantBloc( + hiveHelper: HiveHelper(), + ), + child: Builder( + builder: (context) { + context.read().add( + CheckFavoriteEvent(restaurant: restaurant), + ); + return _Page(restaurant: restaurant); + }, + ), ); } } @@ -31,7 +40,7 @@ class _Page extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - appBar: CustomAppBar(title: restaurant.name), + appBar: CustomAppBar(title: restaurant.name!, restaurant: restaurant), body: _Body(restaurant: restaurant), ); } @@ -74,4 +83,4 @@ class _Body extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart b/lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart index 79728de..9af4f37 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart @@ -1,10 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/restaurant_page/presenter/bloc/restaurant_bloc.dart'; class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { - const CustomAppBar({super.key, this.title}); + const CustomAppBar({ + super.key, + required this.title, + required this.restaurant, + }); - final String? title; + final String title; + final Restaurant restaurant; @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); @@ -14,16 +22,41 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { return AppBar( leading: BackButton( onPressed: () { - context.pop(context); + context.pop(); }, ), - title: Text(title!), + title: Text(title), actions: [ - IconButton( - icon: const Icon(Icons.favorite_border), - onPressed: () {}, - ) + BlocBuilder( + builder: (context, state) { + if (state is LoadingState) { + return const CircularProgressIndicator(); + } else if (state is VerifiedState) { + return IconButton( + icon: Icon( + state.isFavorite ? Icons.favorite : Icons.favorite_border, + color: state.isFavorite ? Colors.red : null, + ), + onPressed: () { + if (!state.isFavorite) { + context.read().add( + AddFavoriteEvent(restaurantId: restaurant.id!), + ); + + } else { + context.read().add( + RemoveFavoriteEvent(restaurantId: restaurant.id!), + ); + + } + }, + ); + } else { + return const SizedBox.shrink(); + } + }, + ), ], ); } -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index 4e019b4..30a2ab5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:restaurant_tour/core/app_init.dart'; import 'package:restaurant_tour/core/navigation/route_navigator.dart'; Future main() async { From 1ec0fd5fab4bdc29565d9cf929ebe5597450a03d Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 12:12:22 -0400 Subject: [PATCH 20/34] feat: added method and widget restaurant to favorites --- lib/core/navigation/route_navigator.dart | 10 ++++++-- .../home_page/presenter/bloc/home_bloc.dart | 10 +++++++- .../home_page/presenter/bloc/home_event.dart | 7 ++++++ .../home_page/presenter/bloc/home_state.dart | 6 ++++- .../home_page/presenter/page/home_screen.dart | 10 ++++---- .../page/widgets/my_favorites_tab.dart | 25 +++++++++++-------- .../presenter/page/widgets/tab_views.dart | 4 ++- .../presenter/bloc/restaurant_event.dart | 7 ++++++ .../page/widgets/custom_app_bar.dart | 8 ++++-- 9 files changed, 64 insertions(+), 23 deletions(-) diff --git a/lib/core/navigation/route_navigator.dart b/lib/core/navigation/route_navigator.dart index f8fa1ba..5d15b69 100644 --- a/lib/core/navigation/route_navigator.dart +++ b/lib/core/navigation/route_navigator.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_page/presenter/bloc/home_bloc.dart'; import 'package:restaurant_tour/features/home_page/presenter/page/home_screen.dart'; import 'package:restaurant_tour/features/restaurant_page/presenter/page/restaurant_screen.dart'; import 'package:restaurant_tour/features/splash_screen/presenter/splash_screen.dart'; @@ -11,13 +14,16 @@ final GoRouter router = GoRouter( GoRoute( path: '/splash', builder: (BuildContext context, GoRouterState state) => - const SplashScreen(), + const SplashScreen(), ), GoRoute( path: '/home', name: 'home', builder: (BuildContext context, GoRouterState state) { - return const HomeScreen(); + return BlocProvider( + create: (_) => HomeBloc(hiveHelper: HiveHelper())..add(const InitialEvent()), + child: const HomeScreen(), + ); }, ), GoRoute( diff --git a/lib/features/home_page/presenter/bloc/home_bloc.dart b/lib/features/home_page/presenter/bloc/home_bloc.dart index 3e43a00..d90deb3 100644 --- a/lib/features/home_page/presenter/bloc/home_bloc.dart +++ b/lib/features/home_page/presenter/bloc/home_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; import 'package:restaurant_tour/repositories/yelp_repository.dart'; @@ -8,10 +9,12 @@ part 'home_event.dart'; part 'home_state.dart'; class HomeBloc extends Bloc { - HomeBloc() : super(HomeInitial()) { + HomeBloc({required this.hiveHelper}) : super(HomeInitial()) { on(_onInitialEvent); } + final HiveHelper hiveHelper; + Future _onInitialEvent( InitialEvent event, Emitter emit, @@ -25,9 +28,14 @@ class HomeBloc extends Bloc { result.when( ok: (data) { if (data.restaurants.isNotEmpty) { + List favoriteIds = hiveHelper.getAllFavoriteIds(); + List favoriteList = data.restaurants + .where((restaurant) => favoriteIds.contains(restaurant.id)) + .toList(); emit( HomeDataLoadedState( restaurantList: data.restaurants, + favoriteList: favoriteList, ), ); } else { diff --git a/lib/features/home_page/presenter/bloc/home_event.dart b/lib/features/home_page/presenter/bloc/home_event.dart index 6ed1979..53db2f6 100644 --- a/lib/features/home_page/presenter/bloc/home_event.dart +++ b/lib/features/home_page/presenter/bloc/home_event.dart @@ -8,6 +8,13 @@ sealed class HomeEvent extends Equatable { class InitialEvent extends HomeEvent { const InitialEvent(); + @override + List get props => []; +} + +class LoadFavoritesEvent extends HomeEvent { + const LoadFavoritesEvent(); + @override List get props => []; } \ No newline at end of file diff --git a/lib/features/home_page/presenter/bloc/home_state.dart b/lib/features/home_page/presenter/bloc/home_state.dart index b62b0bb..c9e1370 100644 --- a/lib/features/home_page/presenter/bloc/home_state.dart +++ b/lib/features/home_page/presenter/bloc/home_state.dart @@ -15,9 +15,13 @@ class HomeLoadingState extends HomeState { } class HomeDataLoadedState extends HomeState { - const HomeDataLoadedState({required this.restaurantList}); + const HomeDataLoadedState({ + required this.restaurantList, + required this.favoriteList, + }); final List restaurantList; + final List favoriteList; @override List get props => [restaurantList]; diff --git a/lib/features/home_page/presenter/page/home_screen.dart b/lib/features/home_page/presenter/page/home_screen.dart index e8e306a..ce0eb91 100644 --- a/lib/features/home_page/presenter/page/home_screen.dart +++ b/lib/features/home_page/presenter/page/home_screen.dart @@ -9,10 +9,7 @@ class HomeScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => HomeBloc()..add(const InitialEvent()), - child: _Page(), - ); + return const _Page(); } } @@ -79,7 +76,10 @@ class _Body extends StatelessWidget { return const HomeLoadingSkeleton(); } if (state is HomeDataLoadedState) { - return TabViews(restaurantList: state.restaurantList); + return TabViews( + restaurantList: state.restaurantList, + favoriteList: state.favoriteList, + ); } else { return const SizedBox(); } diff --git a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart index 89e5425..551a3d3 100644 --- a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart +++ b/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart @@ -1,24 +1,27 @@ import 'package:flutter/material.dart'; -import 'package:restaurant_tour/core/constants.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_page/presenter/page/widgets/card_restaurant.dart'; class MyFavoritesTab extends StatelessWidget { const MyFavoritesTab({ super.key, + required this.favoriteList, }); + final List favoriteList; + @override Widget build(BuildContext context) { - return const Center( - child: Padding( - padding: EdgeInsets.only(top: kPaddingTopTabBar), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text('My Favorites'), - ], - ), - ), + return ListView.builder( + padding: const EdgeInsets.only(top: 8.0), + itemCount: favoriteList.length, + itemBuilder: (context, index) { + final restaurant = favoriteList[index]; + return CardRestaurant( + restaurant: restaurant, + ); + }, ); } } \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/widgets/tab_views.dart b/lib/features/home_page/presenter/page/widgets/tab_views.dart index f1f9c7f..cec53d2 100644 --- a/lib/features/home_page/presenter/page/widgets/tab_views.dart +++ b/lib/features/home_page/presenter/page/widgets/tab_views.dart @@ -7,9 +7,11 @@ class TabViews extends StatelessWidget { const TabViews({ super.key, required this.restaurantList, + required this.favoriteList, }); final List restaurantList; + final List favoriteList; @override Widget build(BuildContext context) { @@ -17,7 +19,7 @@ class TabViews extends StatelessWidget { child: TabBarView( children: [ AllRestaurantsTab(restaurantList: restaurantList), - MyFavoritesTab(), + MyFavoritesTab(favoriteList: favoriteList), ], ), ); diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart b/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart index b74deee..7475621 100644 --- a/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart +++ b/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart @@ -29,4 +29,11 @@ class RemoveFavoriteEvent extends RestaurantEvent { @override List get props => [restaurantId]; +} + +class UpdateListEvent extends RestaurantEvent { + const UpdateListEvent(); + + @override + List get props => []; } \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart b/lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart index 9af4f37..e320679 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart +++ b/lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart @@ -42,12 +42,16 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { context.read().add( AddFavoriteEvent(restaurantId: restaurant.id!), ); - + context.read().add( + CheckFavoriteEvent(restaurant: restaurant), + ); } else { context.read().add( RemoveFavoriteEvent(restaurantId: restaurant.id!), ); - + context.read().add( + CheckFavoriteEvent(restaurant: restaurant), + ); } }, ); From 1c3e1bdb3f0d5fb2bf314b93ac8398bffe5194c2 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 12:42:23 -0400 Subject: [PATCH 21/34] chore: clean architecture concepts applied on files and folders --- lib/core/navigation/route_navigator.dart | 12 +++--------- .../presenter/bloc/home_bloc.dart | 0 .../presenter/bloc/home_event.dart | 0 .../presenter/bloc/home_state.dart | 0 .../presenter/page/home_screen.dart | 14 ++++++++++---- .../presenter/page/home_screen_export.dart | 2 ++ .../page/widgets/all_restaurants_tab.dart | 2 +- .../widgets/card_restaurant}/card_restaurant.dart | 4 ++-- .../card_restaurant/card_restaurant_export.dart | 3 +++ .../card_restaurant}/restaurant_card_skeleton.dart | 0 .../widgets/card_restaurant}/status_indicator.dart | 0 .../page/widgets/home_loading_skeleton.dart | 2 +- .../presenter/page/widgets/my_favorites_tab.dart | 2 +- .../presenter/page/widgets/tab_views.dart | 4 ++-- .../presenter/page/widgets/widgets_export.dart | 5 +++++ .../presenter/bloc/restaurant_bloc.dart | 0 .../presenter/bloc/restaurant_event.dart | 0 .../presenter/bloc/restaurant_state.dart | 0 .../presenter/page/restaurant_screen.dart | 8 ++++---- .../presenter/page/restaurant_screen_export.dart | 2 ++ .../presenter/page/widgets/custom_app_bar.dart | 2 +- .../presenter/page/widgets/raiting_area.dart | 0 .../page/widgets/restaurant_details_area.dart | 2 +- .../presenter/page/widgets/reviews_area.dart | 2 +- .../presenter/page/widgets/widgets_export.dart | 4 ++++ .../page/widgets => shared}/rate_stars.dart | 0 26 files changed, 43 insertions(+), 27 deletions(-) rename lib/features/{home_page => home_screen}/presenter/bloc/home_bloc.dart (100%) rename lib/features/{home_page => home_screen}/presenter/bloc/home_event.dart (100%) rename lib/features/{home_page => home_screen}/presenter/bloc/home_state.dart (100%) rename lib/features/{home_page => home_screen}/presenter/page/home_screen.dart (81%) create mode 100644 lib/features/home_screen/presenter/page/home_screen_export.dart rename lib/features/{home_page => home_screen}/presenter/page/widgets/all_restaurants_tab.dart (84%) rename lib/features/{home_page/presenter/page/widgets => home_screen/presenter/page/widgets/card_restaurant}/card_restaurant.dart (95%) create mode 100644 lib/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant_export.dart rename lib/features/{home_page/presenter/page/widgets => home_screen/presenter/page/widgets/card_restaurant}/restaurant_card_skeleton.dart (100%) rename lib/features/{home_page/presenter/page/widgets => home_screen/presenter/page/widgets/card_restaurant}/status_indicator.dart (100%) rename lib/features/{home_page => home_screen}/presenter/page/widgets/home_loading_skeleton.dart (77%) rename lib/features/{home_page => home_screen}/presenter/page/widgets/my_favorites_tab.dart (84%) rename lib/features/{home_page => home_screen}/presenter/page/widgets/tab_views.dart (74%) create mode 100644 lib/features/home_screen/presenter/page/widgets/widgets_export.dart rename lib/features/{restaurant_page => restaurant_screen}/presenter/bloc/restaurant_bloc.dart (100%) rename lib/features/{restaurant_page => restaurant_screen}/presenter/bloc/restaurant_event.dart (100%) rename lib/features/{restaurant_page => restaurant_screen}/presenter/bloc/restaurant_state.dart (100%) rename lib/features/{restaurant_page => restaurant_screen}/presenter/page/restaurant_screen.dart (84%) create mode 100644 lib/features/restaurant_screen/presenter/page/restaurant_screen_export.dart rename lib/features/{restaurant_page => restaurant_screen}/presenter/page/widgets/custom_app_bar.dart (95%) rename lib/features/{restaurant_page => restaurant_screen}/presenter/page/widgets/raiting_area.dart (100%) rename lib/features/{restaurant_page => restaurant_screen}/presenter/page/widgets/restaurant_details_area.dart (94%) rename lib/features/{restaurant_page => restaurant_screen}/presenter/page/widgets/reviews_area.dart (96%) create mode 100644 lib/features/restaurant_screen/presenter/page/widgets/widgets_export.dart rename lib/{features/home_page/presenter/page/widgets => shared}/rate_stars.dart (100%) diff --git a/lib/core/navigation/route_navigator.dart b/lib/core/navigation/route_navigator.dart index 5d15b69..a3b9b98 100644 --- a/lib/core/navigation/route_navigator.dart +++ b/lib/core/navigation/route_navigator.dart @@ -1,11 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import 'package:restaurant_tour/core/helpers/hive_helper.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/home_page/presenter/bloc/home_bloc.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/home_screen.dart'; -import 'package:restaurant_tour/features/restaurant_page/presenter/page/restaurant_screen.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/page/home_screen.dart'; +import 'package:restaurant_tour/features/restaurant_screen/presenter/page/restaurant_screen.dart'; import 'package:restaurant_tour/features/splash_screen/presenter/splash_screen.dart'; final GoRouter router = GoRouter( @@ -20,10 +17,7 @@ final GoRouter router = GoRouter( path: '/home', name: 'home', builder: (BuildContext context, GoRouterState state) { - return BlocProvider( - create: (_) => HomeBloc(hiveHelper: HiveHelper())..add(const InitialEvent()), - child: const HomeScreen(), - ); + return const HomeScreen(); }, ), GoRoute( diff --git a/lib/features/home_page/presenter/bloc/home_bloc.dart b/lib/features/home_screen/presenter/bloc/home_bloc.dart similarity index 100% rename from lib/features/home_page/presenter/bloc/home_bloc.dart rename to lib/features/home_screen/presenter/bloc/home_bloc.dart diff --git a/lib/features/home_page/presenter/bloc/home_event.dart b/lib/features/home_screen/presenter/bloc/home_event.dart similarity index 100% rename from lib/features/home_page/presenter/bloc/home_event.dart rename to lib/features/home_screen/presenter/bloc/home_event.dart diff --git a/lib/features/home_page/presenter/bloc/home_state.dart b/lib/features/home_screen/presenter/bloc/home_state.dart similarity index 100% rename from lib/features/home_page/presenter/bloc/home_state.dart rename to lib/features/home_screen/presenter/bloc/home_state.dart diff --git a/lib/features/home_page/presenter/page/home_screen.dart b/lib/features/home_screen/presenter/page/home_screen.dart similarity index 81% rename from lib/features/home_page/presenter/page/home_screen.dart rename to lib/features/home_screen/presenter/page/home_screen.dart index ce0eb91..4c49fad 100644 --- a/lib/features/home_page/presenter/page/home_screen.dart +++ b/lib/features/home_screen/presenter/page/home_screen.dart @@ -1,15 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurant_tour/features/home_page/presenter/bloc/home_bloc.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/widgets/home_loading_skeleton.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/widgets/tab_views.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/bloc/home_bloc.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/home_loading_skeleton.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/tab_views.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @override Widget build(BuildContext context) { - return const _Page(); + return BlocProvider( + create: (context) => HomeBloc( + hiveHelper: HiveHelper(), + )..add(const InitialEvent()), + child: const _Page(), + ); } } diff --git a/lib/features/home_screen/presenter/page/home_screen_export.dart b/lib/features/home_screen/presenter/page/home_screen_export.dart new file mode 100644 index 0000000..3a0eda9 --- /dev/null +++ b/lib/features/home_screen/presenter/page/home_screen_export.dart @@ -0,0 +1,2 @@ +export 'widgets/widgets_export.dart'; +export 'home_screen.dart'; \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart b/lib/features/home_screen/presenter/page/widgets/all_restaurants_tab.dart similarity index 84% rename from lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart rename to lib/features/home_screen/presenter/page/widgets/all_restaurants_tab.dart index 5226b09..7ed4a0f 100644 --- a/lib/features/home_page/presenter/page/widgets/all_restaurants_tab.dart +++ b/lib/features/home_screen/presenter/page/widgets/all_restaurants_tab.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/widgets/card_restaurant.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant.dart'; class AllRestaurantsTab extends StatelessWidget { const AllRestaurantsTab({ diff --git a/lib/features/home_page/presenter/page/widgets/card_restaurant.dart b/lib/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant.dart similarity index 95% rename from lib/features/home_page/presenter/page/widgets/card_restaurant.dart rename to lib/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant.dart index 8a53bd0..166ee70 100644 --- a/lib/features/home_page/presenter/page/widgets/card_restaurant.dart +++ b/lib/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/widgets/rate_stars.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/widgets/status_indicator.dart'; +import 'package:restaurant_tour/shared/rate_stars.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/card_restaurant/status_indicator.dart'; class CardRestaurant extends StatelessWidget { diff --git a/lib/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant_export.dart b/lib/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant_export.dart new file mode 100644 index 0000000..7867d2f --- /dev/null +++ b/lib/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant_export.dart @@ -0,0 +1,3 @@ +export 'card_restaurant.dart'; +export 'restaurant_card_skeleton.dart'; +export 'status_indicator.dart'; \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/widgets/restaurant_card_skeleton.dart b/lib/features/home_screen/presenter/page/widgets/card_restaurant/restaurant_card_skeleton.dart similarity index 100% rename from lib/features/home_page/presenter/page/widgets/restaurant_card_skeleton.dart rename to lib/features/home_screen/presenter/page/widgets/card_restaurant/restaurant_card_skeleton.dart diff --git a/lib/features/home_page/presenter/page/widgets/status_indicator.dart b/lib/features/home_screen/presenter/page/widgets/card_restaurant/status_indicator.dart similarity index 100% rename from lib/features/home_page/presenter/page/widgets/status_indicator.dart rename to lib/features/home_screen/presenter/page/widgets/card_restaurant/status_indicator.dart diff --git a/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart b/lib/features/home_screen/presenter/page/widgets/home_loading_skeleton.dart similarity index 77% rename from lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart rename to lib/features/home_screen/presenter/page/widgets/home_loading_skeleton.dart index 1e17a77..d933220 100644 --- a/lib/features/home_page/presenter/page/widgets/home_loading_skeleton.dart +++ b/lib/features/home_screen/presenter/page/widgets/home_loading_skeleton.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/widgets/restaurant_card_skeleton.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/card_restaurant/restaurant_card_skeleton.dart'; class HomeLoadingSkeleton extends StatelessWidget { const HomeLoadingSkeleton({super.key}); diff --git a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_screen/presenter/page/widgets/my_favorites_tab.dart similarity index 84% rename from lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart rename to lib/features/home_screen/presenter/page/widgets/my_favorites_tab.dart index 551a3d3..dd2575b 100644 --- a/lib/features/home_page/presenter/page/widgets/my_favorites_tab.dart +++ b/lib/features/home_screen/presenter/page/widgets/my_favorites_tab.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/widgets/card_restaurant.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant.dart'; class MyFavoritesTab extends StatelessWidget { diff --git a/lib/features/home_page/presenter/page/widgets/tab_views.dart b/lib/features/home_screen/presenter/page/widgets/tab_views.dart similarity index 74% rename from lib/features/home_page/presenter/page/widgets/tab_views.dart rename to lib/features/home_screen/presenter/page/widgets/tab_views.dart index cec53d2..c8620d4 100644 --- a/lib/features/home_page/presenter/page/widgets/tab_views.dart +++ b/lib/features/home_screen/presenter/page/widgets/tab_views.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/widgets/all_restaurants_tab.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/widgets/my_favorites_tab.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/all_restaurants_tab.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/my_favorites_tab.dart'; class TabViews extends StatelessWidget { const TabViews({ diff --git a/lib/features/home_screen/presenter/page/widgets/widgets_export.dart b/lib/features/home_screen/presenter/page/widgets/widgets_export.dart new file mode 100644 index 0000000..685abdf --- /dev/null +++ b/lib/features/home_screen/presenter/page/widgets/widgets_export.dart @@ -0,0 +1,5 @@ +export 'card_restaurant/card_restaurant.dart'; +export 'all_restaurants_tab.dart'; +export 'home_loading_skeleton.dart'; +export 'my_favorites_tab.dart'; +export 'tab_views.dart'; \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart b/lib/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart similarity index 100% rename from lib/features/restaurant_page/presenter/bloc/restaurant_bloc.dart rename to lib/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_event.dart b/lib/features/restaurant_screen/presenter/bloc/restaurant_event.dart similarity index 100% rename from lib/features/restaurant_page/presenter/bloc/restaurant_event.dart rename to lib/features/restaurant_screen/presenter/bloc/restaurant_event.dart diff --git a/lib/features/restaurant_page/presenter/bloc/restaurant_state.dart b/lib/features/restaurant_screen/presenter/bloc/restaurant_state.dart similarity index 100% rename from lib/features/restaurant_page/presenter/bloc/restaurant_state.dart rename to lib/features/restaurant_screen/presenter/bloc/restaurant_state.dart diff --git a/lib/features/restaurant_page/presenter/page/restaurant_screen.dart b/lib/features/restaurant_screen/presenter/page/restaurant_screen.dart similarity index 84% rename from lib/features/restaurant_page/presenter/page/restaurant_screen.dart rename to lib/features/restaurant_screen/presenter/page/restaurant_screen.dart index 955a205..94e5841 100644 --- a/lib/features/restaurant_page/presenter/page/restaurant_screen.dart +++ b/lib/features/restaurant_screen/presenter/page/restaurant_screen.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:restaurant_tour/core/helpers/hive_helper.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/restaurant_page/presenter/bloc/restaurant_bloc.dart'; -import 'package:restaurant_tour/features/restaurant_page/presenter/page/widgets/raiting_area.dart'; -import 'package:restaurant_tour/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart'; -import 'package:restaurant_tour/features/restaurant_page/presenter/page/widgets/reviews_area.dart'; +import 'package:restaurant_tour/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart'; +import 'package:restaurant_tour/features/restaurant_screen/presenter/page/widgets/raiting_area.dart'; +import 'package:restaurant_tour/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart'; +import 'package:restaurant_tour/features/restaurant_screen/presenter/page/widgets/reviews_area.dart'; import 'widgets/custom_app_bar.dart'; diff --git a/lib/features/restaurant_screen/presenter/page/restaurant_screen_export.dart b/lib/features/restaurant_screen/presenter/page/restaurant_screen_export.dart new file mode 100644 index 0000000..4032a9c --- /dev/null +++ b/lib/features/restaurant_screen/presenter/page/restaurant_screen_export.dart @@ -0,0 +1,2 @@ +export 'widgets/widgets_export.dart'; +export 'restaurant_screen.dart'; \ No newline at end of file diff --git a/lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart b/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart similarity index 95% rename from lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart rename to lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart index e320679..c13c317 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/custom_app_bar.dart +++ b/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/restaurant_page/presenter/bloc/restaurant_bloc.dart'; +import 'package:restaurant_tour/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart'; class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { const CustomAppBar({ diff --git a/lib/features/restaurant_page/presenter/page/widgets/raiting_area.dart b/lib/features/restaurant_screen/presenter/page/widgets/raiting_area.dart similarity index 100% rename from lib/features/restaurant_page/presenter/page/widgets/raiting_area.dart rename to lib/features/restaurant_screen/presenter/page/widgets/raiting_area.dart diff --git a/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart b/lib/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart similarity index 94% rename from lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart rename to lib/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart index b2e2052..9e46d32 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/restaurant_details_area.dart +++ b/lib/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/widgets/status_indicator.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/card_restaurant/status_indicator.dart'; class RestaurantDetailsArea extends StatelessWidget { diff --git a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart b/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart similarity index 96% rename from lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart rename to lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart index 8574155..2fd0695 100644 --- a/lib/features/restaurant_page/presenter/page/widgets/reviews_area.dart +++ b/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/home_page/presenter/page/widgets/rate_stars.dart'; +import 'package:restaurant_tour/shared/rate_stars.dart'; class ReviewsArea extends StatelessWidget { const ReviewsArea({ diff --git a/lib/features/restaurant_screen/presenter/page/widgets/widgets_export.dart b/lib/features/restaurant_screen/presenter/page/widgets/widgets_export.dart new file mode 100644 index 0000000..6213978 --- /dev/null +++ b/lib/features/restaurant_screen/presenter/page/widgets/widgets_export.dart @@ -0,0 +1,4 @@ +export 'custom_app_bar.dart'; +export 'raiting_area.dart'; +export 'restaurant_details_area.dart'; +export 'reviews_area.dart'; \ No newline at end of file diff --git a/lib/features/home_page/presenter/page/widgets/rate_stars.dart b/lib/shared/rate_stars.dart similarity index 100% rename from lib/features/home_page/presenter/page/widgets/rate_stars.dart rename to lib/shared/rate_stars.dart From 193b66bec410fbfeffc220807814df49a8709510 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 12:55:57 -0400 Subject: [PATCH 22/34] delete default test generated by sdk --- test/widget_test.dart | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 test/widget_test.dart diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index b729d48..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:restaurant_tour/main.dart'; - -void main() { - testWidgets('Page loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const RestaurantTour()); - - // Verify that tests will run - expect(find.text('Fetch Restaurants'), findsOneWidget); - }); -} From 6bd5caf473a83a4b7bdb80fcad366bbed037ff89 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 13:04:27 -0400 Subject: [PATCH 23/34] feat: added simple test home screen, refactoring home bloc --- .gitignore | 3 +- .../home_screen/presenter/bloc/home_bloc.dart | 5 +- .../presenter/page/home_screen.dart | 2 + pubspec.lock | 108 ++++++++++++++++-- pubspec.yaml | 3 + .../home_screen/bloc/home_bloc_test.dart | 74 ++++++++++++ .../home_screen/bloc/mocks/mock_helpers.dart | 8 ++ 7 files changed, 190 insertions(+), 13 deletions(-) create mode 100644 test/lib/features/home_screen/bloc/home_bloc_test.dart create mode 100644 test/lib/features/home_screen/bloc/mocks/mock_helpers.dart diff --git a/.gitignore b/.gitignore index 1a391ad..92cad8a 100644 --- a/.gitignore +++ b/.gitignore @@ -52,4 +52,5 @@ app.*.map.json .fvmrc # ENV -.env \ No newline at end of file +.env +test/.env.test \ No newline at end of file diff --git a/lib/features/home_screen/presenter/bloc/home_bloc.dart b/lib/features/home_screen/presenter/bloc/home_bloc.dart index d90deb3..004b0c7 100644 --- a/lib/features/home_screen/presenter/bloc/home_bloc.dart +++ b/lib/features/home_screen/presenter/bloc/home_bloc.dart @@ -9,17 +9,18 @@ part 'home_event.dart'; part 'home_state.dart'; class HomeBloc extends Bloc { - HomeBloc({required this.hiveHelper}) : super(HomeInitial()) { + HomeBloc({required this.hiveHelper, required this.yelpRepository}) : super(HomeInitial()) { on(_onInitialEvent); } final HiveHelper hiveHelper; + final YelpRepository yelpRepository; Future _onInitialEvent( InitialEvent event, Emitter emit, ) async { - final yelpRepo = YelpRepository(); + final yelpRepo = yelpRepository; final result = await yelpRepo.getRestaurants(); emit( diff --git a/lib/features/home_screen/presenter/page/home_screen.dart b/lib/features/home_screen/presenter/page/home_screen.dart index 4c49fad..c15dab7 100644 --- a/lib/features/home_screen/presenter/page/home_screen.dart +++ b/lib/features/home_screen/presenter/page/home_screen.dart @@ -4,6 +4,7 @@ import 'package:restaurant_tour/core/helpers/hive_helper.dart'; import 'package:restaurant_tour/features/home_screen/presenter/bloc/home_bloc.dart'; import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/home_loading_skeleton.dart'; import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/tab_views.dart'; +import 'package:restaurant_tour/repositories/yelp_repository.dart'; class HomeScreen extends StatelessWidget { const HomeScreen({super.key}); @@ -13,6 +14,7 @@ class HomeScreen extends StatelessWidget { return BlocProvider( create: (context) => HomeBloc( hiveHelper: HiveHelper(), + yelpRepository: YelpRepository(), )..add(const InitialEvent()), child: const _Page(), ); diff --git a/pubspec.lock b/pubspec.lock index f026024..689c921 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.4" + bloc_test: + dependency: "direct main" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -161,6 +169,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + url: "https://pub.dev" + source: hosted + version: "1.9.2" crypto: dependency: transitive description: @@ -177,6 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" + diff_match_patch: + dependency: transitive + description: + name: diff_match_patch + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" + source: hosted + version: "0.4.1" dio: dependency: "direct main" description: @@ -448,6 +472,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + mocktail: + dependency: "direct main" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" nested: dependency: transitive description: @@ -456,6 +488,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" oxidized: dependency: "direct main" description: @@ -584,14 +624,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.0.4" shimmer: dependency: "direct main" description: @@ -621,6 +677,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" source_span: dependency: transitive description: @@ -669,6 +741,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + url: "https://pub.dev" + source: hosted + version: "1.25.2" test_api: dependency: transitive description: @@ -677,6 +757,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + url: "https://pub.dev" + source: hosted + version: "0.6.0" timing: dependency: transitive description: @@ -725,22 +813,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - web_socket: + web_socket_channel: dependency: transitive description: - name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "0.1.6" - web_socket_channel: + version: "2.4.0" + webkit_inspection_protocol: dependency: transitive description: - name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "1.2.1" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6bcebea..0dec6a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,7 @@ environment: flutter: ">=3.19.6" dependencies: + bloc_test: ^9.1.7 dio: ^5.7.0 equatable: ^2.0.5 flutter: @@ -22,6 +23,7 @@ dependencies: hive_flutter: ^1.1.0 http: ^1.2.2 json_annotation: ^4.9.0 + mocktail: ^1.0.4 oxidized: ^6.2.0 path_provider: ^2.1.4 shimmer: ^3.0.0 @@ -38,6 +40,7 @@ flutter: uses-material-design: true assets: - .env + - test/.env.test - assets/images/ - assets/ fonts: diff --git a/test/lib/features/home_screen/bloc/home_bloc_test.dart b/test/lib/features/home_screen/bloc/home_bloc_test.dart new file mode 100644 index 0000000..fb79a19 --- /dev/null +++ b/test/lib/features/home_screen/bloc/home_bloc_test.dart @@ -0,0 +1,74 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/bloc/home_bloc.dart'; +import 'package:restaurant_tour/repositories/yelp_repository.dart'; + + +class MockHiveHelper extends Mock implements HiveHelper {} + +class MockYelpRepository extends Mock implements YelpRepository {} + +void main() { + setUpAll(() async { + TestWidgetsFlutterBinding.ensureInitialized(); + await dotenv.load( + fileName: "test/.env.test", + ); + }); + + group('HomeBloc', () { + late HomeBloc homeBloc; + late HiveHelper mockHiveHelper; + late YelpRepository mockYelpRepository; + + setUp(() { + mockHiveHelper = MockHiveHelper(); + mockYelpRepository = MockYelpRepository(); + homeBloc = HomeBloc( + hiveHelper: mockHiveHelper, + yelpRepository: mockYelpRepository, + ); + }); + + test('initial state is HomeInitial', () { + expect(homeBloc.state, HomeInitial()); + }); + + blocTest( + 'emits [HomeLoadingState, HomeDataLoadedState] when HomeInitialEvent is added', + build: () { + when(() => mockHiveHelper.getAllFavoriteIds()).thenReturn(['1', '2']); + when(() => mockYelpRepository.getRestaurants()).thenAnswer( + (_) async => const Ok( + RestaurantQueryResult( + total: 1, + restaurants: [ + Restaurant( + id: '1', + name: 'Fake Restaurant', + ), + ], + ), + ), + ); + + return homeBloc; + }, + act: (bloc) => bloc.add(const InitialEvent()), + expect: () => [ + HomeLoadingState(), + isA(), + ], + ); + + + tearDown(() { + homeBloc.close(); + }); + }); +} \ No newline at end of file diff --git a/test/lib/features/home_screen/bloc/mocks/mock_helpers.dart b/test/lib/features/home_screen/bloc/mocks/mock_helpers.dart new file mode 100644 index 0000000..8aa64af --- /dev/null +++ b/test/lib/features/home_screen/bloc/mocks/mock_helpers.dart @@ -0,0 +1,8 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/repositories/yelp_repository.dart'; + + +class MockHiveHelper extends Mock implements HiveHelper {} + +class MockYelpRepository extends Mock implements YelpRepository {} \ No newline at end of file From 0a5bbb51cdfbd5eb2367ce1a44c4123a6998970a Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 15:04:03 -0400 Subject: [PATCH 24/34] feat: added data and domain layers for home screen childrens-clean arch --- ios/Podfile.lock | 30 +++++ ios/Runner.xcodeproj/project.pbxproj | 112 ++++++++++++++++++ .../contents.xcworkspacedata | 3 + lib/core/helpers/dio_helper.dart | 16 +++ lib/core/models/Failure.dart | 27 +++++ lib/core/navigation/route_navigator.dart | 2 +- lib/features/home_screen/home_screen.dart | 69 +++++++++++ .../all_restaurant/all_restaurant_bloc.dart | 52 ++++++++ .../all_restaurant/all_restaurant_event.dart | 12 ++ .../all_restaurant/all_restaurant_state.dart | 42 +++++++ .../presenter/page/all_restaurants_tab.dart | 91 ++++++++++++++ .../card_restaurant/card_restaurant.dart | 16 ++- .../card_restaurant_export.dart | 0 .../restaurant_card_skeleton.dart | 0 .../card_restaurant/status_indicator.dart | 0 .../page/widgets/my_favorites_tab.dart | 12 ++ .../data/api/favorite_restaurants_api.dart | 76 ++++++++++++ .../data/models/category_model.dart | 25 ++++ .../data/models/hour_model.dart | 21 ++++ .../data/models/location_model.dart | 18 +++ .../data/models/restaurant_model.dart | 68 +++++++++++ .../data/models/review_model.dart | 34 ++++++ .../data/models/user_model.dart | 29 +++++ .../domain/entities/category_entity.dart | 9 ++ .../domain/entities/hour_entity.dart | 7 ++ .../domain/entities/location_entity.dart | 7 ++ .../domain/entities/restaurant_entity.dart | 28 +++++ .../domain/entities/review_entity.dart | 16 +++ .../domain/entities/user_entity.dart | 11 ++ .../favorite_restaurant_respository.dart | 9 ++ .../bloc/favorite_restaurants_bloc.dart | 56 +++++++++ .../bloc/favorite_restaurants_event.dart | 12 ++ .../bloc/favorite_restaurants_state.dart | 40 +++++++ .../page/favorite_restaurants_tab.dart | 97 +++++++++++++++ .../presenter/children/widgets/tab_views.dart | 21 ++++ .../presenter/page/home_screen.dart | 100 ---------------- .../presenter/page/home_screen_export.dart | 2 - .../page/widgets/all_restaurants_tab.dart | 24 ---- .../page/widgets/home_loading_skeleton.dart | 20 ---- .../page/widgets/my_favorites_tab.dart | 27 ----- .../presenter/page/widgets/tab_views.dart | 27 ----- .../page/widgets/widgets_export.dart | 5 - .../presenter/bloc/restaurant_bloc.dart | 6 +- .../presenter/bloc/restaurant_state.dart | 4 +- .../page/widgets/custom_app_bar.dart | 30 ++--- .../page/widgets/restaurant_details_area.dart | 3 +- .../presenter/page/widgets/reviews_area.dart | 23 ++-- lib/repositories/yelp_repository.dart | 2 +- lib/shared/widgets/home_loading_skeleton.dart | 19 +++ pubspec.lock | 96 +++++++++++++++ pubspec.yaml | 2 + 51 files changed, 1214 insertions(+), 244 deletions(-) create mode 100644 ios/Podfile.lock create mode 100644 lib/core/helpers/dio_helper.dart create mode 100644 lib/core/models/Failure.dart create mode 100644 lib/features/home_screen/home_screen.dart create mode 100644 lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart create mode 100644 lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart create mode 100644 lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart create mode 100644 lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart rename lib/features/home_screen/presenter/{ => children/all_restaurant/presenter}/page/widgets/card_restaurant/card_restaurant.dart (85%) rename lib/features/home_screen/presenter/{ => children/all_restaurant/presenter}/page/widgets/card_restaurant/card_restaurant_export.dart (100%) rename lib/features/home_screen/presenter/{ => children/all_restaurant/presenter}/page/widgets/card_restaurant/restaurant_card_skeleton.dart (100%) rename lib/features/home_screen/presenter/{ => children/all_restaurant/presenter}/page/widgets/card_restaurant/status_indicator.dart (100%) create mode 100644 lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/data/api/favorite_restaurants_api.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/location_entity.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/restaurant_entity.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/user_entity.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart create mode 100644 lib/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart create mode 100644 lib/features/home_screen/presenter/children/widgets/tab_views.dart delete mode 100644 lib/features/home_screen/presenter/page/home_screen.dart delete mode 100644 lib/features/home_screen/presenter/page/home_screen_export.dart delete mode 100644 lib/features/home_screen/presenter/page/widgets/all_restaurants_tab.dart delete mode 100644 lib/features/home_screen/presenter/page/widgets/home_loading_skeleton.dart delete mode 100644 lib/features/home_screen/presenter/page/widgets/my_favorites_tab.dart delete mode 100644 lib/features/home_screen/presenter/page/widgets/tab_views.dart delete mode 100644 lib/features/home_screen/presenter/page/widgets/widgets_export.dart create mode 100644 lib/shared/widgets/home_loading_skeleton.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..e2deba1 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,30 @@ +PODS: + - Flutter (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqflite (0.0.3): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - Flutter (from `Flutter`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + sqflite: + :path: ".symlinks/plugins/sqflite/darwin" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + +PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 + +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 182fb57..2116ea8 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 4B0DF36E64CCC78A0927054A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 772B3BF4F53AD3FF5A302193 /* Pods_Runner.framework */; }; + 63073288EC73C36144C3C578 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6C412E8DAE05ABA81EF2D0B9 /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; @@ -45,9 +47,15 @@ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 443B1126BE6B7A3DB4519ACF /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 6C412E8DAE05ABA81EF2D0B9 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 772B3BF4F53AD3FF5A302193 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7F01E83E5D88FEEA5870B61B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 8EE6BAFB4FBCA09E239AB09F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 95210C47D340FEA50229F657 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -55,6 +63,8 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + EA57161C465EA1831F6B4141 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + FBCF9E09C3DE9AEFAF254D69 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -62,6 +72,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4B0DF36E64CCC78A0927054A /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F76702391D514442B55A3556 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 63073288EC73C36144C3C578 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -76,6 +95,29 @@ path = RunnerTests; sourceTree = ""; }; + 3B59A012BA108304F39D0DF6 /* Pods */ = { + isa = PBXGroup; + children = ( + 8EE6BAFB4FBCA09E239AB09F /* Pods-Runner.debug.xcconfig */, + FBCF9E09C3DE9AEFAF254D69 /* Pods-Runner.release.xcconfig */, + EA57161C465EA1831F6B4141 /* Pods-Runner.profile.xcconfig */, + 443B1126BE6B7A3DB4519ACF /* Pods-RunnerTests.debug.xcconfig */, + 7F01E83E5D88FEEA5870B61B /* Pods-RunnerTests.release.xcconfig */, + 95210C47D340FEA50229F657 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 61089AA88EE44CF92294A999 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 772B3BF4F53AD3FF5A302193 /* Pods_Runner.framework */, + 6C412E8DAE05ABA81EF2D0B9 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -94,6 +136,8 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, + 3B59A012BA108304F39D0DF6 /* Pods */, + 61089AA88EE44CF92294A999 /* Frameworks */, ); sourceTree = ""; }; @@ -128,8 +172,10 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + DA18E9261DA580AE50697825 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, + F76702391D514442B55A3556 /* Frameworks */, ); buildRules = ( ); @@ -145,12 +191,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 7D522D8C5743B12FC570D77C /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 0EBED81A90CEC178B4170D74 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -222,6 +270,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 0EBED81A90CEC178B4170D74 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -238,6 +303,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 7D522D8C5743B12FC570D77C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -253,6 +340,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + DA18E9261DA580AE50697825 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -379,6 +488,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 443B1126BE6B7A3DB4519ACF /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -396,6 +506,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 7F01E83E5D88FEEA5870B61B /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -411,6 +522,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 95210C47D340FEA50229F657 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/lib/core/helpers/dio_helper.dart b/lib/core/helpers/dio_helper.dart new file mode 100644 index 0000000..ac98cad --- /dev/null +++ b/lib/core/helpers/dio_helper.dart @@ -0,0 +1,16 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class DioHelper { + static final Dio _dio = Dio( + BaseOptions( + baseUrl: 'https://api.yelp.com', + headers: { + 'Authorization': 'Bearer ${dotenv.env['YELP_API_KEY']}', + 'Content-Type': 'application/graphql', + }, + ), + ); + + static Dio get dio => _dio; +} \ No newline at end of file diff --git a/lib/core/models/Failure.dart b/lib/core/models/Failure.dart new file mode 100644 index 0000000..9016642 --- /dev/null +++ b/lib/core/models/Failure.dart @@ -0,0 +1,27 @@ +import 'package:equatable/equatable.dart'; + +abstract class Failure extends Equatable { + final String message; + final int? statusCode; + + const Failure({ + required this.message, + this.statusCode = 0, + }); + + @override + List get props => [message]; +} + +class ServerFailure extends Failure { + const ServerFailure({ + required String message, + int? statusCode, + }) : super(message: message, statusCode: statusCode); +} + +class LocalFailure extends Failure { + const LocalFailure({ + required String message, + }) : super(message: message); +} \ No newline at end of file diff --git a/lib/core/navigation/route_navigator.dart b/lib/core/navigation/route_navigator.dart index a3b9b98..991409e 100644 --- a/lib/core/navigation/route_navigator.dart +++ b/lib/core/navigation/route_navigator.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/page/home_screen.dart'; +import 'package:restaurant_tour/features/home_screen/home_screen.dart'; import 'package:restaurant_tour/features/restaurant_screen/presenter/page/restaurant_screen.dart'; import 'package:restaurant_tour/features/splash_screen/presenter/splash_screen.dart'; diff --git a/lib/features/home_screen/home_screen.dart b/lib/features/home_screen/home_screen.dart new file mode 100644 index 0000000..3e7e422 --- /dev/null +++ b/lib/features/home_screen/home_screen.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/widgets/tab_views.dart'; + + +class HomeScreen extends StatelessWidget { + const HomeScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const _Page(); + } +} + +class _Page extends StatelessWidget { + const _Page(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'RestauranTour', + style: TextStyle(fontWeight: FontWeight.w700), + ), + ), + body: const _Body(), + ); + } +} + +class _Body extends StatelessWidget { + const _Body(); + + @override + Widget build(BuildContext context) { + return const DefaultTabController( + length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Material( + elevation: 6.0, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.0), + topRight: Radius.circular(10.0), + ), + child: TabBar( + indicator: UnderlineTabIndicator( + borderSide: BorderSide( + color: Colors.black, + width: 2.0, + ), + ), + labelColor: Colors.black, + unselectedLabelColor: Colors.grey, + tabs: [ + Tab( + text: 'All Restaurants', + ), + Tab(text: 'My Favorites'), + ], + ), + ), + TabViews(), + ], + ), + ); + } +} diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart new file mode 100644 index 0000000..4fa4f34 --- /dev/null +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/repositories/yelp_repository.dart'; + +part 'all_restaurant_event.dart'; +part 'all_restaurant_state.dart'; + +class AllRestaurantBloc extends Bloc { + AllRestaurantBloc({required this.hiveHelper, required this.yelpRepository}) + : super(AllRestaurantInitial()) { + on(_onInitEvent); + } + + final HiveHelper hiveHelper; + final YelpRepository yelpRepository; + + Future _onInitEvent( + InitialEvent event, + Emitter emit, + ) async { + emit( + const LoadingState(), + ); + final yelpRepo = yelpRepository; + final result = await yelpRepo.getRestaurants(); + + result.when( + ok: (data) { + if (data.restaurants.isNotEmpty) { + emit( + DataLoadedState( + restaurantList: data.restaurants, + ), + ); + } else { + emit(const EmptyDataState()); + } + }, + err: (error) { + emit( + ErrorState( + error: error.toString(), + ), + ); + }, + ); + } +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart new file mode 100644 index 0000000..dd5a211 --- /dev/null +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart @@ -0,0 +1,12 @@ +part of 'all_restaurant_bloc.dart'; + +sealed class AllRestaurantEvent extends Equatable { + const AllRestaurantEvent(); +} + +class InitialEvent extends AllRestaurantEvent { + const InitialEvent(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart new file mode 100644 index 0000000..a0d3c66 --- /dev/null +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart @@ -0,0 +1,42 @@ +part of 'all_restaurant_bloc.dart'; + +abstract class AllRestaurantState extends Equatable { + const AllRestaurantState(); +} + +class AllRestaurantInitial extends AllRestaurantState { + @override + List get props => []; +} + +class LoadingState extends AllRestaurantState { + const LoadingState(); + + @override + List get props => []; +} + +class DataLoadedState extends AllRestaurantState { + const DataLoadedState({required this.restaurantList}); + + final List restaurantList; + + @override + List get props => []; +} + +class EmptyDataState extends AllRestaurantState { + const EmptyDataState(); + + @override + List get props => []; +} + +class ErrorState extends AllRestaurantState { + const ErrorState({required this.error}); + + final String error; + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart new file mode 100644 index 0000000..23eb3c3 --- /dev/null +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart @@ -0,0 +1,91 @@ + +import 'package:animate_do/animate_do.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart'; + +import 'package:restaurant_tour/repositories/yelp_repository.dart'; +import 'package:restaurant_tour/shared/widgets/home_loading_skeleton.dart'; + + +class AllRestaurantsTab extends StatelessWidget { + const AllRestaurantsTab({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => AllRestaurantBloc( + hiveHelper: HiveHelper(), + yelpRepository: YelpRepository(), + )..add(const InitialEvent()), + child: const _Page(), + ); + } +} + +class _Page extends StatelessWidget { + const _Page({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is ErrorState) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Something went wrong, please come back later.'), + duration: Duration(seconds: 3), + ), + ); + } + }, + child: const _Body(), + ); + } +} + +class _Body extends StatelessWidget { + const _Body({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is LoadingState) { + return const CardsLoadingSkeleton(); + } + if (state is DataLoadedState) { + return ListView.builder( + padding: const EdgeInsets.only(top: 8.0), + itemCount: state.restaurantList.length, + itemBuilder: (context, index) { + final restaurant = state.restaurantList[index]; + final int delay = index * 500; + return FadeInRight( + child: CardRestaurant( + restaurant: restaurant, + ), + delay: Duration(milliseconds: delay), + ); + }, + ); + } + if (state is EmptyDataState) { + return const Center( + child: Text('No data found'), + ); + } else { + return const SizedBox.shrink(); + } + }, + ); + } +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart similarity index 85% rename from lib/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant.dart rename to lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart index 166ee70..39bb52a 100644 --- a/lib/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant.dart +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart @@ -1,8 +1,12 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/status_indicator.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart'; import 'package:restaurant_tour/shared/rate_stars.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/card_restaurant/status_indicator.dart'; + class CardRestaurant extends StatelessWidget { @@ -17,7 +21,11 @@ class CardRestaurant extends StatelessWidget { Widget build(BuildContext context) { return InkWell( onTap: () { - context.pushNamed('restaurant-screen', extra: restaurant); + context.pushNamed('restaurant-screen', extra: restaurant).then((result) { + if (result == true) { + context.read().add(const InitialEvent()); + } + }); }, child: Padding( padding: const EdgeInsets.symmetric( @@ -37,8 +45,8 @@ class CardRestaurant extends StatelessWidget { height: 90, child: ClipRRect( borderRadius: BorderRadius.circular(5), - child: Image.network( - restaurant.photos!.first, + child: CachedNetworkImage( + imageUrl: restaurant.photos!.first, fit: BoxFit.cover, ), ), diff --git a/lib/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant_export.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant_export.dart similarity index 100% rename from lib/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant_export.dart rename to lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant_export.dart diff --git a/lib/features/home_screen/presenter/page/widgets/card_restaurant/restaurant_card_skeleton.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/restaurant_card_skeleton.dart similarity index 100% rename from lib/features/home_screen/presenter/page/widgets/card_restaurant/restaurant_card_skeleton.dart rename to lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/restaurant_card_skeleton.dart diff --git a/lib/features/home_screen/presenter/page/widgets/card_restaurant/status_indicator.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/status_indicator.dart similarity index 100% rename from lib/features/home_screen/presenter/page/widgets/card_restaurant/status_indicator.dart rename to lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/status_indicator.dart diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart new file mode 100644 index 0000000..b7c27e5 --- /dev/null +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class MyFavoritesTab extends StatelessWidget { + const MyFavoritesTab({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Text('data'); + } +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/api/favorite_restaurants_api.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/api/favorite_restaurants_api.dart new file mode 100644 index 0000000..6d1e384 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/api/favorite_restaurants_api.dart @@ -0,0 +1,76 @@ +import 'package:dio/dio.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurant_tour/core/helpers/dio_helper.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.dart'; + + + +class FavoriteRestaurantsApi extends FavoriteRestaurantsRepository { + final Dio dio; + + FavoriteRestaurantsApi({Dio? dio}) : dio = dio ?? DioHelper.dio; + + String _getRestaurantDetailsQuery(String restaurantId) { + return ''' + { + business(id: "$restaurantId") { + id + name + price + rating + photos + reviews { + id + rating + text + user { + id + image_url + name + } + } + categories { + title + alias + } + hours { + is_open_now + } + location { + formatted_address + } + } + } + '''; + } + + @override + Future> getRestaurantDetails({ + required String restaurantId, + }) async { + try { + final response = await dio.post>( + '/v3/graphql', + data: _getRestaurantDetailsQuery(restaurantId), + ); + + if (response.data != null) { + final result = RestaurantModel.fromJson(response.data!); + return Ok(result); + } else { + return Err(DioException( + error: 'La respuesta no contiene datos', + requestOptions: RequestOptions(path: '/v3/graphql'), + )); + } + } on DioException catch (e) { + return Err(e); + } catch (e) { + return Err(DioException( + error: 'Error desconocido: $e', + requestOptions: RequestOptions(path: '/v3/graphql'), + )); + } + } +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart new file mode 100644 index 0000000..bad0ae2 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart @@ -0,0 +1,25 @@ +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart'; + +class CategoryModel extends CategoryEntity { + const CategoryModel({ + required String title, + required String alias, + }) : super( + title: title, + alias: alias, + ); + + Map toJson() { + return { + 'title': title, + 'alias': alias, + }; + } + + factory CategoryModel.fromJson(Map json) { + return CategoryModel( + title: json['title'] ?? '', + alias: json['alias'] ?? '', + ); + } +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart new file mode 100644 index 0000000..a3dee4f --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart @@ -0,0 +1,21 @@ +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart'; + +class HourModel extends HourEntity { + const HourModel({ + required bool isOpenNow, + }) : super( + isOpenNow: isOpenNow, + ); + + Map toJson() { + return { + 'is_open_now': isOpenNow, + }; + } + + factory HourModel.fromJson(Map json) { + return HourModel( + isOpenNow: json['is_open_now'] as bool, + ); + } +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart new file mode 100644 index 0000000..63645ba --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart @@ -0,0 +1,18 @@ +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/location_entity.dart'; + +class LocationModel extends LocationEntity { + const LocationModel({required String formattedAddress}) + : super(formattedAddress: formattedAddress); + + factory LocationModel.fromJson(Map json) { + return LocationModel( + formattedAddress: json['formatted_address'] ?? '', + ); + } + + Map toJson() { + return { + 'formatted_address': formattedAddress, + }; + } +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart new file mode 100644 index 0000000..4f9be91 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart @@ -0,0 +1,68 @@ + + +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/restaurant_entity.dart'; + +class RestaurantModel extends RestaurantEntity { + const RestaurantModel({ + required super.id, + required super.name, + required super.price, + required super.rating, + required super.photos, + required super.reviews, + required super.categories, + required super.hours, + required super.location, + }); + + Map toJson() { + return { + 'id': id, + 'name': name, + 'price': price, + 'rating': rating, + 'photos': photos, + 'reviews': reviews.map((review) => (review).toJson()).toList(), + 'categories': categories.map((category) => (category).toJson()).toList(), + 'hours': hours.map((hour) => (hour).toJson()).toList(), + 'location': (location).toJson(), + }; + } + + factory RestaurantModel.fromJson(Map json) { + var businessJson = json['data']['business'] as Map; + final id = businessJson['id']; + final name = businessJson['name'] ?? ''; + final price = businessJson['price'] ?? ''; + final rating = (businessJson['rating'] ?? 0.0).toDouble(); + final photos = List.from(businessJson['photos'] ?? []); + final reviews = (businessJson['reviews'] as List? ?? []) + .map((x) => ReviewModel.fromJson(x as Map)) + .toList(); + final categories = (businessJson['categories'] as List? ?? []) + .map((x) => CategoryModel.fromJson(x as Map)) + .toList(); + final hours = (businessJson['hours'] as List? ?? []) + .map((x) => HourModel.fromJson(x as Map)) + .toList(); + final locationJson = businessJson['location'] as Map?; + final location = LocationModel.fromJson(locationJson!); + + return RestaurantModel( + id: id, + name: name, + price: price, + rating: rating, + photos: photos, + reviews: reviews, + categories: categories, + hours: hours, + location:location + ); + } + +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart new file mode 100644 index 0000000..d419f21 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart @@ -0,0 +1,34 @@ +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart'; + +class ReviewModel extends ReviewEntity { + const ReviewModel({ + required String id, + required int rating, + required String text, + required UserModel user, + }) : super( + id: id, + rating: rating, + text: text, + user: user, + ); + + Map toJson() { + return { + 'id': id, + 'rating': rating, + 'text': text, + 'user': user.toMap(), + }; + } + + factory ReviewModel.fromJson(Map json) { + return ReviewModel( + id: json['id'] ?? '', + rating: json['rating'] ?? 0.0, + text: json['text'] ?? '', + user: UserModel.fromJson(json['user'] as Map), + ); + } +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart new file mode 100644 index 0000000..37fca31 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart @@ -0,0 +1,29 @@ +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/user_entity.dart'; + +class UserModel extends UserEntity { + const UserModel({ + required String id, + required String name, + required String imageUrl, + }) : super( + id: id, + name: name, + imageUrl: imageUrl, + ); + + factory UserModel.fromJson(Map json) { + return UserModel( + id: json['id'] ?? '', + name: json['name'] ?? '', + imageUrl: json['image_url'] ?? '', + ); + } + + Map toMap() { + return { + 'id': id, + 'name': name, + 'image_url': imageUrl, + }; + } +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart new file mode 100644 index 0000000..fc5838b --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart @@ -0,0 +1,9 @@ +class CategoryEntity { + const CategoryEntity({ + required this.title, + required this.alias, + }); + + final String title; + final String alias; +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart new file mode 100644 index 0000000..65e4c04 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart @@ -0,0 +1,7 @@ +class HourEntity { + const HourEntity({ + required this.isOpenNow, + }); + + final bool isOpenNow; +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/location_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/location_entity.dart new file mode 100644 index 0000000..e0f8fe6 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/location_entity.dart @@ -0,0 +1,7 @@ +class LocationEntity { + final String formattedAddress; + + const LocationEntity({ + required this.formattedAddress, + }); +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/restaurant_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/restaurant_entity.dart new file mode 100644 index 0000000..cadd363 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/restaurant_entity.dart @@ -0,0 +1,28 @@ +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart'; + +class RestaurantEntity { + final String id; + final String name; + final String price; + final double rating; + final List photos; + final List reviews; + final List categories; + final List hours; + final LocationModel location; + + const RestaurantEntity({ + required this.id, + required this.name, + required this.price, + required this.rating, + required this.photos, + required this.reviews, + required this.categories, + required this.hours, + required this.location, + }); +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart new file mode 100644 index 0000000..52d0e88 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart @@ -0,0 +1,16 @@ + +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart'; + +class ReviewEntity { + const ReviewEntity({ + required this.id, + required this.rating, + required this.text, + required this.user, + }); + + final String id; + final int rating; + final String text; + final UserModel user; +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/user_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/user_entity.dart new file mode 100644 index 0000000..adec656 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/user_entity.dart @@ -0,0 +1,11 @@ +class UserEntity { + final String id; + final String imageUrl; + final String name; + + const UserEntity({ + required this.id, + required this.imageUrl, + required this.name, + }); +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.dart new file mode 100644 index 0000000..32d48f3 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.dart @@ -0,0 +1,9 @@ +import 'package:dio/dio.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart'; + +abstract class FavoriteRestaurantsRepository { + Future> getRestaurantDetails({ + required String restaurantId, + }); +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart new file mode 100644 index 0000000..3dfd1ae --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart @@ -0,0 +1,56 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.dart'; + +part 'favorite_restaurants_event.dart'; +part 'favorite_restaurants_state.dart'; + +class FavoriteRestaurantsBloc + extends Bloc { + FavoriteRestaurantsBloc({ + required this.hiveHelper, + required this.favoriteRestaurantsRepository, + }) : super(FavoriteRestaurantsInitial()) { + on(_onInitialEvent); + } + + final HiveHelper hiveHelper; + final FavoriteRestaurantsRepository favoriteRestaurantsRepository; + + Future _onInitialEvent( + InitialEvent event, + Emitter emit, + ) async { + emit(const LoadingState()); + + final favoriteList = hiveHelper.getAllFavoriteIds(); + List favoriteRestaurants = []; + + if (favoriteList.isEmpty) { + emit(const NoFavoritesState()); + return; + } + + for (var restaurantId in favoriteList) { + final response = await favoriteRestaurantsRepository.getRestaurantDetails( + restaurantId: restaurantId, + ); + + response.when( + ok: (restaurant) { + favoriteRestaurants.add(restaurant); + }, + err: (err) { + print(err); + return; + }, + ); + } + + emit(FavoriteRestaurantsLoaded(favoriteList: favoriteRestaurants)); + } +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart new file mode 100644 index 0000000..e314510 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart @@ -0,0 +1,12 @@ +part of 'favorite_restaurants_bloc.dart'; + +sealed class FavoriteRestaurantsEvent extends Equatable { + const FavoriteRestaurantsEvent(); +} + +class InitialEvent extends FavoriteRestaurantsEvent { + const InitialEvent(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart new file mode 100644 index 0000000..b614395 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart @@ -0,0 +1,40 @@ +part of 'favorite_restaurants_bloc.dart'; + +abstract class FavoriteRestaurantsState extends Equatable { + const FavoriteRestaurantsState(); +} + +class FavoriteRestaurantsInitial extends FavoriteRestaurantsState { + @override + List get props => []; +} + +class LoadingState extends FavoriteRestaurantsState { + const LoadingState(); + + @override + List get props => []; +} + +class FavoriteRestaurantsLoaded extends FavoriteRestaurantsState { + const FavoriteRestaurantsLoaded({required this.favoriteList}); + + final List favoriteList; + + @override + List get props => [favoriteList]; +} + +class NoFavoritesState extends FavoriteRestaurantsState { + const NoFavoritesState(); + + @override + List get props => []; +} + +class FavErrorState extends FavoriteRestaurantsState { + const FavErrorState(); + + @override + List get props => []; +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart new file mode 100644 index 0000000..48be956 --- /dev/null +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart @@ -0,0 +1,97 @@ +import 'package:animate_do/animate_do.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/api/favorite_restaurants_api.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart'; +import 'package:restaurant_tour/shared/widgets/home_loading_skeleton.dart'; + + +class FavoriteRestaurantsTab extends StatelessWidget { + const FavoriteRestaurantsTab({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => FavoriteRestaurantsBloc( + hiveHelper: HiveHelper(), + favoriteRestaurantsRepository: FavoriteRestaurantsApi(), + )..add( + const InitialEvent(), + ), + child: const _Page(), + ); + } +} + +class _Page extends StatelessWidget { + const _Page({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (state is FavErrorState) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Something went wrong, please come back later.'), + duration: Duration(seconds: 3), + ), + ); + } + }, + child: const _Body(), + ); + } +} + +class _Body extends StatelessWidget { + const _Body({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is LoadingState) { + return const CardsLoadingSkeleton(); + } + if (state is FavoriteRestaurantsLoaded) { + return ListView.builder( + padding: const EdgeInsets.only(top: 8.0), + itemCount: state.favoriteList.length, + itemBuilder: (context, index) { + final restaurant = state.favoriteList[index]; + final parseRestaurant = Restaurant.fromJson(restaurant.toJson()); + final int delay = index * 500; + return FadeInRight( + child: CardRestaurant( + restaurant: parseRestaurant, + ), + delay: Duration(milliseconds: delay), + ); + }, + ); + } + if (state is NoFavoritesState) { + return const Center( + child: Text( + 'No favorite restaurantes were added', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + ); + } else { + return const Text('No data found'); + } + }, + ); + } +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/widgets/tab_views.dart b/lib/features/home_screen/presenter/children/widgets/tab_views.dart new file mode 100644 index 0000000..66d824c --- /dev/null +++ b/lib/features/home_screen/presenter/children/widgets/tab_views.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart'; + +class TabViews extends StatelessWidget { + const TabViews({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return const Flexible( + child: TabBarView( + children: [ + AllRestaurantsTab(), + FavoriteRestaurantsTab(), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/page/home_screen.dart b/lib/features/home_screen/presenter/page/home_screen.dart deleted file mode 100644 index c15dab7..0000000 --- a/lib/features/home_screen/presenter/page/home_screen.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:restaurant_tour/core/helpers/hive_helper.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/bloc/home_bloc.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/home_loading_skeleton.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/tab_views.dart'; -import 'package:restaurant_tour/repositories/yelp_repository.dart'; - -class HomeScreen extends StatelessWidget { - const HomeScreen({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => HomeBloc( - hiveHelper: HiveHelper(), - yelpRepository: YelpRepository(), - )..add(const InitialEvent()), - child: const _Page(), - ); - } -} - -class _Page extends StatelessWidget { - const _Page({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return BlocListener( - listener: (context, state) { - // TODO: implement listener - }, - child: Scaffold( - appBar: AppBar( - title: const Text( - 'RestauranTour', - style: TextStyle(fontWeight: FontWeight.w700), - ), - ), - body: _Body(), - ), - ); - } -} - -class _Body extends StatelessWidget { - const _Body({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return DefaultTabController( - length: 2, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Material( - elevation: 6.0, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10.0), - topRight: Radius.circular(10.0), - ), - child: TabBar( - indicator: UnderlineTabIndicator( - borderSide: BorderSide( - color: Colors.black, - width: 2.0, - ), - ), - labelColor: Colors.black, - unselectedLabelColor: Colors.grey, - tabs: [ - Tab(text: 'All Restaurants'), - Tab(text: 'My Favorites'), - ], - ), - ), - BlocBuilder( - builder: (context, state) { - if (state is HomeLoadingState) { - return const HomeLoadingSkeleton(); - } - if (state is HomeDataLoadedState) { - return TabViews( - restaurantList: state.restaurantList, - favoriteList: state.favoriteList, - ); - } else { - return const SizedBox(); - } - }, - ), - ], - ), - ); - } -} diff --git a/lib/features/home_screen/presenter/page/home_screen_export.dart b/lib/features/home_screen/presenter/page/home_screen_export.dart deleted file mode 100644 index 3a0eda9..0000000 --- a/lib/features/home_screen/presenter/page/home_screen_export.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'widgets/widgets_export.dart'; -export 'home_screen.dart'; \ No newline at end of file diff --git a/lib/features/home_screen/presenter/page/widgets/all_restaurants_tab.dart b/lib/features/home_screen/presenter/page/widgets/all_restaurants_tab.dart deleted file mode 100644 index 7ed4a0f..0000000 --- a/lib/features/home_screen/presenter/page/widgets/all_restaurants_tab.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant.dart'; - -class AllRestaurantsTab extends StatelessWidget { - const AllRestaurantsTab({ - super.key, - required this.restaurantList, - }); - - final List restaurantList; - - @override - Widget build(BuildContext context) { - return ListView.builder( - padding: const EdgeInsets.only(top: 8.0), - itemCount: restaurantList.length, - itemBuilder: (context, index) { - final restaurant = restaurantList[index]; - return CardRestaurant(restaurant: restaurant); - }, - ); - } -} diff --git a/lib/features/home_screen/presenter/page/widgets/home_loading_skeleton.dart b/lib/features/home_screen/presenter/page/widgets/home_loading_skeleton.dart deleted file mode 100644 index d933220..0000000 --- a/lib/features/home_screen/presenter/page/widgets/home_loading_skeleton.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/card_restaurant/restaurant_card_skeleton.dart'; - -class HomeLoadingSkeleton extends StatelessWidget { - const HomeLoadingSkeleton({super.key}); - - @override - Widget build(BuildContext context) { - const int itemCount = 6; - - return Expanded( - child: ListView.builder( - itemCount: itemCount, - itemBuilder: (BuildContext context, int index) { - return const RestaurantCardSkeleton(); - }, - ), - ); - } -} diff --git a/lib/features/home_screen/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_screen/presenter/page/widgets/my_favorites_tab.dart deleted file mode 100644 index dd2575b..0000000 --- a/lib/features/home_screen/presenter/page/widgets/my_favorites_tab.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/card_restaurant/card_restaurant.dart'; - - -class MyFavoritesTab extends StatelessWidget { - const MyFavoritesTab({ - super.key, - required this.favoriteList, - }); - - final List favoriteList; - - @override - Widget build(BuildContext context) { - return ListView.builder( - padding: const EdgeInsets.only(top: 8.0), - itemCount: favoriteList.length, - itemBuilder: (context, index) { - final restaurant = favoriteList[index]; - return CardRestaurant( - restaurant: restaurant, - ); - }, - ); - } -} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/page/widgets/tab_views.dart b/lib/features/home_screen/presenter/page/widgets/tab_views.dart deleted file mode 100644 index c8620d4..0000000 --- a/lib/features/home_screen/presenter/page/widgets/tab_views.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/all_restaurants_tab.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/my_favorites_tab.dart'; - -class TabViews extends StatelessWidget { - const TabViews({ - super.key, - required this.restaurantList, - required this.favoriteList, - }); - - final List restaurantList; - final List favoriteList; - - @override - Widget build(BuildContext context) { - return Flexible( - child: TabBarView( - children: [ - AllRestaurantsTab(restaurantList: restaurantList), - MyFavoritesTab(favoriteList: favoriteList), - ], - ), - ); - } -} diff --git a/lib/features/home_screen/presenter/page/widgets/widgets_export.dart b/lib/features/home_screen/presenter/page/widgets/widgets_export.dart deleted file mode 100644 index 685abdf..0000000 --- a/lib/features/home_screen/presenter/page/widgets/widgets_export.dart +++ /dev/null @@ -1,5 +0,0 @@ -export 'card_restaurant/card_restaurant.dart'; -export 'all_restaurants_tab.dart'; -export 'home_loading_skeleton.dart'; -export 'my_favorites_tab.dart'; -export 'tab_views.dart'; \ No newline at end of file diff --git a/lib/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart b/lib/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart index 31d58fc..a179942 100644 --- a/lib/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart +++ b/lib/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart @@ -19,7 +19,7 @@ class RestaurantBloc extends Bloc { CheckFavoriteEvent event, Emitter emit, ) async { - emit(const LoadingState()); + emit(const AppBarLoadingState()); try { List favoriteIds = hiveHelper.getAllFavoriteIds(); bool isFavorite = favoriteIds.contains(event.restaurant.id); @@ -34,7 +34,7 @@ class RestaurantBloc extends Bloc { AddFavoriteEvent event, Emitter emit, ) async { - emit(const LoadingState()); + emit(const AppBarLoadingState()); try { await hiveHelper.addFavorite(event.restaurantId); emit(const VerifiedState(isFavorite: true)); @@ -47,7 +47,7 @@ class RestaurantBloc extends Bloc { RemoveFavoriteEvent event, Emitter emit, ) async { - emit(const LoadingState()); + emit(const AppBarLoadingState()); try { await hiveHelper.removeFavorite(event.restaurantId); emit(const VerifiedState(isFavorite: false)); diff --git a/lib/features/restaurant_screen/presenter/bloc/restaurant_state.dart b/lib/features/restaurant_screen/presenter/bloc/restaurant_state.dart index ba4df9a..060c998 100644 --- a/lib/features/restaurant_screen/presenter/bloc/restaurant_state.dart +++ b/lib/features/restaurant_screen/presenter/bloc/restaurant_state.dart @@ -9,8 +9,8 @@ final class RestaurantInitial extends RestaurantState { List get props => []; } -class LoadingState extends RestaurantState { - const LoadingState(); +class AppBarLoadingState extends RestaurantState { + const AppBarLoadingState(); @override List get props => []; diff --git a/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart b/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart index c13c317..ec8b528 100644 --- a/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart +++ b/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart @@ -19,39 +19,35 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { + bool isFavorite = false; return AppBar( leading: BackButton( onPressed: () { - context.pop(); + context.pop(!isFavorite); }, ), title: Text(title), actions: [ BlocBuilder( builder: (context, state) { - if (state is LoadingState) { + if (state is AppBarLoadingState) { return const CircularProgressIndicator(); } else if (state is VerifiedState) { + isFavorite = state.isFavorite; return IconButton( icon: Icon( - state.isFavorite ? Icons.favorite : Icons.favorite_border, - color: state.isFavorite ? Colors.red : null, + isFavorite ? Icons.favorite : Icons.favorite_border, + color: isFavorite ? Colors.red : null, ), onPressed: () { - if (!state.isFavorite) { - context.read().add( - AddFavoriteEvent(restaurantId: restaurant.id!), - ); - context.read().add( - CheckFavoriteEvent(restaurant: restaurant), - ); + if (!isFavorite) { + context + .read() + .add(AddFavoriteEvent(restaurantId: restaurant.id!)); } else { - context.read().add( - RemoveFavoriteEvent(restaurantId: restaurant.id!), - ); - context.read().add( - CheckFavoriteEvent(restaurant: restaurant), - ); + context + .read() + .add(RemoveFavoriteEvent(restaurantId: restaurant.id!)); } }, ); diff --git a/lib/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart b/lib/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart index 9e46d32..1ab97af 100644 --- a/lib/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart +++ b/lib/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/page/widgets/card_restaurant/status_indicator.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant_export.dart'; + class RestaurantDetailsArea extends StatelessWidget { diff --git a/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart b/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart index 2fd0695..94ca671 100644 --- a/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart +++ b/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart @@ -1,3 +1,4 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; import 'package:restaurant_tour/shared/rate_stars.dart'; @@ -37,20 +38,22 @@ class ReviewsArea extends StatelessWidget { Padding( padding: const EdgeInsets.only(right: 8.0), child: ClipOval( - child: Image.network( - review.user?.imageUrl ?? + child: CachedNetworkImage( + imageUrl: review.user?.imageUrl ?? 'https://fakeimg.pl/600x400', width: 40, height: 40, fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return Image.network( - 'https://fakeimg.pl/600x400', - width: 40, - height: 40, - fit: BoxFit.cover, - ); - }, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => + Image.network( + 'https://fakeimg.pl/600x400', + width: 40, + height: 40, + fit: BoxFit.cover, + ), ), ), ), diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index e6f283e..b2445a2 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/repositories/yelp_repository.dart @@ -57,7 +57,7 @@ class YelpRepository { String _getQuery(int offset) { return ''' - query getRestaurants { + query getRestaurants($offset: Int) { search(location: "Las Vegas", limit: 20, offset: $offset) { total business { diff --git a/lib/shared/widgets/home_loading_skeleton.dart b/lib/shared/widgets/home_loading_skeleton.dart new file mode 100644 index 0000000..7278ba2 --- /dev/null +++ b/lib/shared/widgets/home_loading_skeleton.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/restaurant_card_skeleton.dart'; + + +class CardsLoadingSkeleton extends StatelessWidget { + const CardsLoadingSkeleton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + const int itemCount = 6; + + return ListView.builder( + itemCount: itemCount, + itemBuilder: (BuildContext context, int index) { + return const RestaurantCardSkeleton(); + }, + ); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 689c921..281d950 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.4.1" + animate_do: + dependency: "direct main" + description: + name: animate_do + sha256: "7a3162729f0ea042f9dd84da217c5bde5472ad9cef644079929d4304a5dc4ca0" + url: "https://pub.dev" + source: hosted + version: "3.3.4" args: dependency: transitive description: @@ -121,6 +129,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.9.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" characters: dependency: transitive description: @@ -270,6 +302,14 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.6" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_dotenv: dependency: "direct main" description: @@ -496,6 +536,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" oxidized: dependency: "direct main" description: @@ -616,6 +664,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" shelf: dependency: transitive description: @@ -701,6 +757,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + url: "https://pub.dev" + source: hosted + version: "2.3.3+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + url: "https://pub.dev" + source: hosted + version: "2.5.4" stack_trace: dependency: transitive description: @@ -733,6 +813,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -781,6 +869,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + url: "https://pub.dev" + source: hosted + version: "4.5.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0dec6a9..1c928ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,9 @@ environment: flutter: ">=3.19.6" dependencies: + animate_do: ^3.3.4 bloc_test: ^9.1.7 + cached_network_image: ^3.4.1 dio: ^5.7.0 equatable: ^2.0.5 flutter: From 7aed4b47927afe0f2dab8e1658d1ed5ad501b735 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 15:12:38 -0400 Subject: [PATCH 25/34] feat: added user comments to reviews --- .../presenter/page/all_restaurants_tab.dart | 8 +- .../page/widgets/my_favorites_tab.dart | 2 +- .../presenter/page/widgets/reviews_area.dart | 77 +++++++++++-------- 3 files changed, 47 insertions(+), 40 deletions(-) diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart index 23eb3c3..09a91de 100644 --- a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart @@ -28,9 +28,7 @@ class AllRestaurantsTab extends StatelessWidget { } class _Page extends StatelessWidget { - const _Page({ - super.key, - }); + const _Page(); @override Widget build(BuildContext context) { @@ -51,9 +49,7 @@ class _Page extends StatelessWidget { } class _Body extends StatelessWidget { - const _Body({ - super.key, - }); + const _Body(); @override Widget build(BuildContext context) { diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart index b7c27e5..c300317 100644 --- a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart @@ -7,6 +7,6 @@ class MyFavoritesTab extends StatelessWidget { @override Widget build(BuildContext context) { - return Text('data'); + return const Text('data'); } } \ No newline at end of file diff --git a/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart b/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart index 94ca671..be06c6b 100644 --- a/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart +++ b/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart @@ -26,49 +26,60 @@ class ReviewsArea extends StatelessWidget { ), if (reviews != null) for (Review review in reviews!) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: RateStars(rate: review.rating?.toDouble() ?? 0.0), - ), - Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: ClipOval( - child: CachedNetworkImage( - imageUrl: review.user?.imageUrl ?? - 'https://fakeimg.pl/600x400', - width: 40, - height: 40, - fit: BoxFit.cover, - placeholder: (context, url) => const Center( - child: CircularProgressIndicator(), - ), - errorWidget: (context, url, error) => + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.9, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: + RateStars(rate: review.rating?.toDouble() ?? 0.0), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + review.text ?? '', + style: const TextStyle(fontSize: 15), + ), + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ClipOval( + child: CachedNetworkImage( + imageUrl: review.user?.imageUrl ?? + 'https://fakeimg.pl/600x400', + width: 40, + height: 40, + fit: BoxFit.cover, + placeholder: (context, url) => const Center( + child: CircularProgressIndicator(), + ), + errorWidget: (context, url, error) => Image.network( 'https://fakeimg.pl/600x400', width: 40, height: 40, fit: BoxFit.cover, ), + ), ), ), + Text(review.user?.name ?? ''), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Container( + color: Colors.grey.shade300, + height: 1, + width: MediaQuery.sizeOf(context).width * 0.9, ), - Text(review.user?.name ?? ''), - ], - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Container( - color: Colors.grey.shade300, - height: 1, - width: MediaQuery.sizeOf(context).width * 0.9, ), - ), - ], + ], + ), ), ], ), From 5b81c0fc50e5a4cd74d46a52d01abe8a8099288e Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 15:28:11 -0400 Subject: [PATCH 26/34] feat: added unit testing for all restaurants and favorite restaurants --- .../home_screen/bloc/home_bloc_test.dart | 74 ----------- .../bloc/all_restaurant_bloc_test.dart | 123 ++++++++++++++++++ .../domain/entities/category_entity_test.dart | 59 +++++++++ .../domain/entities/hour_entity_test.dart | 43 ++++++ .../entities/restaurant_entity_test.dart | 54 ++++++++ .../bloc/mocks => }/mock_helpers.dart | 0 6 files changed, 279 insertions(+), 74 deletions(-) delete mode 100644 test/lib/features/home_screen/bloc/home_bloc_test.dart create mode 100644 test/lib/features/home_screen/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart create mode 100644 test/lib/features/home_screen/children/favorite_restaurants/domain/entities/category_entity_test.dart create mode 100644 test/lib/features/home_screen/children/favorite_restaurants/domain/entities/hour_entity_test.dart create mode 100644 test/lib/features/home_screen/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart rename test/lib/{features/home_screen/bloc/mocks => }/mock_helpers.dart (100%) diff --git a/test/lib/features/home_screen/bloc/home_bloc_test.dart b/test/lib/features/home_screen/bloc/home_bloc_test.dart deleted file mode 100644 index fb79a19..0000000 --- a/test/lib/features/home_screen/bloc/home_bloc_test.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:oxidized/oxidized.dart'; -import 'package:restaurant_tour/core/helpers/hive_helper.dart'; -import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/bloc/home_bloc.dart'; -import 'package:restaurant_tour/repositories/yelp_repository.dart'; - - -class MockHiveHelper extends Mock implements HiveHelper {} - -class MockYelpRepository extends Mock implements YelpRepository {} - -void main() { - setUpAll(() async { - TestWidgetsFlutterBinding.ensureInitialized(); - await dotenv.load( - fileName: "test/.env.test", - ); - }); - - group('HomeBloc', () { - late HomeBloc homeBloc; - late HiveHelper mockHiveHelper; - late YelpRepository mockYelpRepository; - - setUp(() { - mockHiveHelper = MockHiveHelper(); - mockYelpRepository = MockYelpRepository(); - homeBloc = HomeBloc( - hiveHelper: mockHiveHelper, - yelpRepository: mockYelpRepository, - ); - }); - - test('initial state is HomeInitial', () { - expect(homeBloc.state, HomeInitial()); - }); - - blocTest( - 'emits [HomeLoadingState, HomeDataLoadedState] when HomeInitialEvent is added', - build: () { - when(() => mockHiveHelper.getAllFavoriteIds()).thenReturn(['1', '2']); - when(() => mockYelpRepository.getRestaurants()).thenAnswer( - (_) async => const Ok( - RestaurantQueryResult( - total: 1, - restaurants: [ - Restaurant( - id: '1', - name: 'Fake Restaurant', - ), - ], - ), - ), - ); - - return homeBloc; - }, - act: (bloc) => bloc.add(const InitialEvent()), - expect: () => [ - HomeLoadingState(), - isA(), - ], - ); - - - tearDown(() { - homeBloc.close(); - }); - }); -} \ No newline at end of file diff --git a/test/lib/features/home_screen/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart b/test/lib/features/home_screen/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart new file mode 100644 index 0000000..fc61e1e --- /dev/null +++ b/test/lib/features/home_screen/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart @@ -0,0 +1,123 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:oxidized/oxidized.dart'; +import 'package:restaurant_tour/core/helpers/hive_helper.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart'; +import 'package:restaurant_tour/repositories/yelp_repository.dart'; + + +class MockYelpRepository extends Mock implements YelpRepository {} + +class MockHiveHelper extends Mock implements HiveHelper {} + +class MockDioException extends Mock implements DioException {} + +var mockCategories = [ + Category(title: 'Italian', alias: 'italian'), + Category(title: 'Mexican', alias: 'mexican'), +]; + +var mockReviews = [ + const Review( + id: '3a2sd1', + rating: 5, + text: 'Amazing experience!', + user: User( + id: 'user1', + name: 'John Doe', + imageUrl: 'https://example.com/user1.jpg', + ), + ), + const Review( + id: '3a2sd1f3', + rating: 4, + text: 'Great food, will come again.', + user: User( + id: 'user2', + name: 'Jane Smith', + imageUrl: 'https://example.com/user2.jpg', + ), + ), +]; + +var mockRestaurants = [ + Restaurant( + id: '1', + name: 'Mock Italian Restaurant', + rating: 4.5, + photos: ['https://example.com/restaurant1.jpg'], + categories: mockCategories, + reviews: mockReviews, + ), + Restaurant( + id: '2', + name: 'Mock Mexican Restaurant', + rating: 4.0, + photos: ['https://example.com/restaurant2.jpg'], + categories: mockCategories, + reviews: mockReviews, + ), +]; + +void main() { + group( + 'AllRestaurantBloc', + () { + late YelpRepository yelpRepository; + late AllRestaurantBloc allRestaurantBloc; + late HiveHelper hiveHelper; + + setUp(() { + yelpRepository = MockYelpRepository(); + hiveHelper = MockHiveHelper(); + allRestaurantBloc = AllRestaurantBloc( + hiveHelper: hiveHelper, + yelpRepository: yelpRepository, + ); + + registerFallbackValue(Uri()); + }); + + blocTest( + 'emits [LoadingState, DataLoadedState] when restaurants are fetched successfully', + build: () { + when(() => yelpRepository.getRestaurants()).thenAnswer( + (_) async => Result.ok( + RestaurantQueryResult( + restaurants: mockRestaurants, + ), + ), + ); + return allRestaurantBloc; + }, + act: (bloc) => bloc.add( + const InitialEvent(), + ), + expect: () => [ + const LoadingState(), + isA(), + ], + ); + + blocTest( + 'emits [LoadingState, ErrorState] when fetching restaurants fails', + build: () { + final dioError = MockDioException(); + + when(() => yelpRepository.getRestaurants()).thenAnswer( + (_) async => Result.err(dioError), + ); + return allRestaurantBloc; + }, + act: (bloc) => bloc.add(const InitialEvent()), + expect: () => [ + const LoadingState(), + isA(), + ], + ); + }, + ); +} diff --git a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/category_entity_test.dart b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/category_entity_test.dart new file mode 100644 index 0000000..96c1a12 --- /dev/null +++ b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/category_entity_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart'; + + +void main() { + group( + 'CategoryEntity', + () { + test('should have the correct properties', () { + const title = 'Italian'; + const alias = 'italian'; + const categoryEntity = CategoryEntity( + title: title, + alias: alias, + ); + + expect(categoryEntity.title, title); + expect(categoryEntity.alias, alias); + }); + + test( + 'should support value equality', + () { + const categoryEntity1 = CategoryEntity( + title: 'Italian', + alias: 'italian', + ); + const categoryEntity2 = CategoryEntity( + title: 'Italian', + alias: 'italian', + ); + + expect( + categoryEntity1, + equals( + categoryEntity2, + ), + ); + }, + ); + + test('should not be equal when properties differ', () { + const categoryEntity1 = + CategoryEntity(title: 'Italian', alias: 'italian'); + const categoryEntity2 = + CategoryEntity(title: 'Mexican', alias: 'mexican'); + + expect( + categoryEntity1, + isNot( + equals( + categoryEntity2, + ), + ), + ); + }); + }, + ); +} \ No newline at end of file diff --git a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/hour_entity_test.dart b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/hour_entity_test.dart new file mode 100644 index 0000000..5eb3d01 --- /dev/null +++ b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/hour_entity_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart'; + + +void main() { + group( + 'HourEntity', + () { + test('should have isOpenNow property correctly assigned', () { + const isOpenNow = true; + const hourEntity = HourEntity(isOpenNow: isOpenNow); + + expect(hourEntity.isOpenNow, isOpenNow); + }); + + test('should support value equality based on isOpenNow', () { + const hourEntity1 = HourEntity(isOpenNow: true); + const hourEntity2 = HourEntity(isOpenNow: true); + + expect( + hourEntity1, + equals( + hourEntity2, + ), + ); + }); + + test('should not be equal when isOpenNow values differ', () { + const hourEntity1 = HourEntity(isOpenNow: true); + const hourEntity2 = HourEntity(isOpenNow: false); + + expect( + hourEntity1, + isNot( + equals( + hourEntity2, + ), + ), + ); + }); + }, + ); +} \ No newline at end of file diff --git a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart new file mode 100644 index 0000000..9db4a77 --- /dev/null +++ b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart @@ -0,0 +1,54 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart'; +import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/restaurant_entity.dart'; + +void main() { + group('RestaurantEntity', () { + + const id = '1'; + const name = 'Test Restaurant'; + const price = '\$\$'; + const rating = 4.5; + final photos = ['photo1.jpg', 'photo2.jpg']; + final reviews = [ + const ReviewModel( + id: 'review1', + user: UserModel(id: 'asdf', name: 'TEST USER', imageUrl: ''), + rating: 5, + text: 'Great!') + ]; + final categories = [ + const CategoryModel(title: 'Italian', alias: 'italian') + ]; + final hours = [const HourModel(isOpenNow: true)]; + const location = LocationModel(formattedAddress: '123 Test St'); + + test('should correctly assign properties', () { + final restaurantEntity = RestaurantEntity( + id: id, + name: name, + price: price, + rating: rating, + photos: photos, + reviews: reviews, + categories: categories, + hours: hours, + location: location, + ); + + expect(restaurantEntity.id, id); + expect(restaurantEntity.name, name); + expect(restaurantEntity.price, price); + expect(restaurantEntity.rating, rating); + expect(restaurantEntity.photos, photos); + expect(restaurantEntity.reviews, reviews); + expect(restaurantEntity.categories, categories); + expect(restaurantEntity.hours, hours); + expect(restaurantEntity.location, location); + }); + }); +} \ No newline at end of file diff --git a/test/lib/features/home_screen/bloc/mocks/mock_helpers.dart b/test/lib/mock_helpers.dart similarity index 100% rename from test/lib/features/home_screen/bloc/mocks/mock_helpers.dart rename to test/lib/mock_helpers.dart From 8b2e5b4b669664e58440cb1ac311e8076f66f0c3 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 15:51:10 -0400 Subject: [PATCH 27/34] feat: added onTap action on card restaurant --- .../presenter/page/all_restaurants_tab.dart | 1 + .../card_restaurant/card_restaurant.dart | 48 ++++++++++++------- .../page/favorite_restaurants_tab.dart | 1 + 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart index 09a91de..56aaaef 100644 --- a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart @@ -68,6 +68,7 @@ class _Body extends StatelessWidget { return FadeInRight( child: CardRestaurant( restaurant: restaurant, + isFromFavorites: false, ), delay: Duration(milliseconds: delay), ); diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart index 39bb52a..ff98cf9 100644 --- a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart @@ -7,26 +7,21 @@ import 'package:restaurant_tour/features/home_screen/presenter/children/all_rest import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart'; import 'package:restaurant_tour/shared/rate_stars.dart'; - - class CardRestaurant extends StatelessWidget { const CardRestaurant({ super.key, required this.restaurant, + required this.isFromFavorites, }); final Restaurant restaurant; + final bool isFromFavorites; @override Widget build(BuildContext context) { return InkWell( - onTap: () { - context.pushNamed('restaurant-screen', extra: restaurant).then((result) { - if (result == true) { - context.read().add(const InitialEvent()); - } - }); - }, + onTap: () => onTap(context, + restaurant: restaurant, isFromFavorites: isFromFavorites), child: Padding( padding: const EdgeInsets.symmetric( vertical: 4.0, @@ -76,7 +71,7 @@ class CardRestaurant extends StatelessWidget { ), Padding( padding: - const EdgeInsets.symmetric(horizontal: 4.0), + const EdgeInsets.symmetric(horizontal: 4.0), child: Text( restaurant.categories?.first.title ?? '', style: Theme.of(context).textTheme.bodyMedium, @@ -97,16 +92,16 @@ class CardRestaurant extends StatelessWidget { const Spacer(), Padding( padding: - const EdgeInsets.symmetric(vertical: 8.0), + const EdgeInsets.symmetric(vertical: 8.0), child: restaurant.isOpen ? const StatusIndicator( - text: "Open Now", - color: Colors.green, - ) + text: "Open Now", + color: Colors.green, + ) : const StatusIndicator( - text: "Closed", - color: Colors.red, - ), + text: "Closed", + color: Colors.red, + ), ), ], ), @@ -121,4 +116,21 @@ class CardRestaurant extends StatelessWidget { ), ); } -} \ No newline at end of file + + onTap( + BuildContext context, { + required Restaurant restaurant, + required bool isFromFavorites, + }) { + context.pushNamed('restaurant-page', extra: restaurant,) + .then( + (result) { + if (result == true && isFromFavorites) { + context.read().add( + const InitialEvent(), + ); + } + }, + ); + } +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart index 48be956..2927613 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart @@ -71,6 +71,7 @@ class _Body extends StatelessWidget { final int delay = index * 500; return FadeInRight( child: CardRestaurant( + isFromFavorites:true, restaurant: parseRestaurant, ), delay: Duration(milliseconds: delay), From 0171d8b15f0719d1d4ca8488a43622d46f9db067 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 16:06:11 -0400 Subject: [PATCH 28/34] feat: format fixes and typos --- lib/core/app_init.dart | 3 +- lib/core/constants.dart | 2 +- lib/core/helpers/dio_helper.dart | 2 +- lib/core/helpers/hive_helper.dart | 5 ++-- lib/core/models/Failure.dart | 12 ++++---- lib/core/models/restaurant.dart | 4 +-- lib/core/navigation/route_navigator.dart | 4 +-- lib/features/home_screen/home_screen.dart | 19 ++++++++---- .../home_screen/presenter/bloc/home_bloc.dart | 11 +++---- .../presenter/bloc/home_event.dart | 3 +- .../all_restaurant/all_restaurant_bloc.dart | 12 ++++---- .../all_restaurant/all_restaurant_event.dart | 2 +- .../all_restaurant/all_restaurant_state.dart | 2 +- .../presenter/page/all_restaurants_tab.dart | 6 ++-- .../card_restaurant/card_restaurant.dart | 13 +++++++-- .../card_restaurant_export.dart | 2 +- .../card_restaurant/status_indicator.dart | 2 +- .../page/widgets/my_favorites_tab.dart | 2 +- .../data/api/favorite_restaurants_api.dart | 24 ++++++++------- .../data/models/category_model.dart | 29 ++++++++----------- .../data/models/hour_model.dart | 15 +++++----- .../data/models/location_model.dart | 5 ++-- .../data/models/restaurant_model.dart | 25 +++++++--------- .../data/models/review_model.dart | 17 ++++------- .../data/models/user_model.dart | 14 ++++----- .../domain/entities/category_entity.dart | 13 +++------ .../domain/entities/hour_entity.dart | 8 +---- .../domain/entities/location_entity.dart | 2 +- .../domain/entities/restaurant_entity.dart | 2 +- .../domain/entities/review_entity.dart | 3 +- .../domain/entities/user_entity.dart | 2 +- .../favorite_restaurant_respository.dart | 2 +- .../bloc/favorite_restaurants_bloc.dart | 8 ++--- .../bloc/favorite_restaurants_event.dart | 2 +- .../bloc/favorite_restaurants_state.dart | 2 +- .../page/favorite_restaurants_tab.dart | 21 +++++--------- .../presenter/children/widgets/tab_views.dart | 2 +- .../presenter/bloc/restaurant_bloc.dart | 20 ++++++------- .../presenter/bloc/restaurant_event.dart | 2 +- .../presenter/bloc/restaurant_state.dart | 2 +- .../presenter/page/restaurant_screen.dart | 4 +-- .../page/restaurant_screen_export.dart | 2 +- .../page/widgets/custom_app_bar.dart | 18 ++++++++++-- .../presenter/page/widgets/raiting_area.dart | 2 +- .../page/widgets/restaurant_details_area.dart | 18 +++++------- .../presenter/page/widgets/reviews_area.dart | 12 ++++---- .../page/widgets/widgets_export.dart | 2 +- .../presenter/bloc/splash_screen_bloc.dart | 7 +++-- .../presenter/bloc/splash_screen_event.dart | 2 +- .../presenter/bloc/splash_screen_state.dart | 2 +- .../presenter/splash_screen.dart | 10 ++----- lib/main.dart | 2 +- lib/repositories/yelp_repository.dart | 5 ++-- lib/shared/rate_stars.dart | 6 ++-- lib/shared/widgets/home_loading_skeleton.dart | 5 ++-- .../bloc/all_restaurant_bloc_test.dart | 7 ++--- .../domain/entities/category_entity_test.dart | 11 ++++--- .../domain/entities/hour_entity_test.dart | 5 ++-- .../entities/restaurant_entity_test.dart | 14 ++++----- test/lib/mock_helpers.dart | 3 +- 60 files changed, 223 insertions(+), 240 deletions(-) diff --git a/lib/core/app_init.dart b/lib/core/app_init.dart index b283ad5..1263b83 100644 --- a/lib/core/app_init.dart +++ b/lib/core/app_init.dart @@ -1,10 +1,9 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:restaurant_tour/core/helpers/hive_helper.dart'; - class AppInit { static Future initializeApp() async { await dotenv.load(fileName: ".env"); await HiveHelper().init(); } -} \ No newline at end of file +} diff --git a/lib/core/constants.dart b/lib/core/constants.dart index f84b0f3..13a5aa0 100644 --- a/lib/core/constants.dart +++ b/lib/core/constants.dart @@ -1 +1 @@ -const double kPaddingTopTabBar = 32.0; \ No newline at end of file +const double kPaddingTopTabBar = 32.0; diff --git a/lib/core/helpers/dio_helper.dart b/lib/core/helpers/dio_helper.dart index ac98cad..99d053a 100644 --- a/lib/core/helpers/dio_helper.dart +++ b/lib/core/helpers/dio_helper.dart @@ -13,4 +13,4 @@ class DioHelper { ); static Dio get dio => _dio; -} \ No newline at end of file +} diff --git a/lib/core/helpers/hive_helper.dart b/lib/core/helpers/hive_helper.dart index 58e2d17..07b3b96 100644 --- a/lib/core/helpers/hive_helper.dart +++ b/lib/core/helpers/hive_helper.dart @@ -1,7 +1,6 @@ import 'package:hive/hive.dart'; import 'package:path_provider/path_provider.dart' as path_provider; - class HiveHelper { static final HiveHelper _singleton = HiveHelper._internal(); late Box box; @@ -14,7 +13,7 @@ class HiveHelper { Future init() async { final appDocumentDir = - await path_provider.getApplicationDocumentsDirectory(); + await path_provider.getApplicationDocumentsDirectory(); Hive.init(appDocumentDir.path); box = await Hive.openBox('favorites'); } @@ -36,4 +35,4 @@ class HiveHelper { List getAllFavoriteIds() { return box.get('favoriteIds', defaultValue: [])!.cast(); } - } \ No newline at end of file +} diff --git a/lib/core/models/Failure.dart b/lib/core/models/Failure.dart index 9016642..a10bcf4 100644 --- a/lib/core/models/Failure.dart +++ b/lib/core/models/Failure.dart @@ -15,13 +15,13 @@ abstract class Failure extends Equatable { class ServerFailure extends Failure { const ServerFailure({ - required String message, - int? statusCode, - }) : super(message: message, statusCode: statusCode); + required super.message, + super.statusCode = null, + }); } class LocalFailure extends Failure { const LocalFailure({ - required String message, - }) : super(message: message); -} \ No newline at end of file + required super.message, + }); +} diff --git a/lib/core/models/restaurant.dart b/lib/core/models/restaurant.dart index 74fd69f..4883196 100644 --- a/lib/core/models/restaurant.dart +++ b/lib/core/models/restaurant.dart @@ -154,8 +154,8 @@ class RestaurantQueryResult { return RestaurantQueryResult( total: json['total'] as int?, restaurants: (json['business'] as List?) - ?.map((e) => Restaurant.fromJson(e as Map)) - .toList() ?? + ?.map((e) => Restaurant.fromJson(e as Map)) + .toList() ?? [], ); } diff --git a/lib/core/navigation/route_navigator.dart b/lib/core/navigation/route_navigator.dart index 991409e..33ae32a 100644 --- a/lib/core/navigation/route_navigator.dart +++ b/lib/core/navigation/route_navigator.dart @@ -11,7 +11,7 @@ final GoRouter router = GoRouter( GoRoute( path: '/splash', builder: (BuildContext context, GoRouterState state) => - const SplashScreen(), + const SplashScreen(), ), GoRoute( path: '/home', @@ -29,4 +29,4 @@ final GoRouter router = GoRouter( }, ), ], -); \ No newline at end of file +); diff --git a/lib/features/home_screen/home_screen.dart b/lib/features/home_screen/home_screen.dart index 3e7e422..1d6bc1e 100644 --- a/lib/features/home_screen/home_screen.dart +++ b/lib/features/home_screen/home_screen.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/widgets/tab_views.dart'; - class HomeScreen extends StatelessWidget { - const HomeScreen({Key? key}) : super(key: key); + const HomeScreen({super.key}); @override Widget build(BuildContext context) { @@ -18,9 +17,19 @@ class _Page extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text( - 'RestauranTour', - style: TextStyle(fontWeight: FontWeight.w700), + backgroundColor: Colors.white, + title: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'RestauranTour', + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w700, + color: Colors.black, + ), + ), + ], ), ), body: const _Body(), diff --git a/lib/features/home_screen/presenter/bloc/home_bloc.dart b/lib/features/home_screen/presenter/bloc/home_bloc.dart index 004b0c7..39340bf 100644 --- a/lib/features/home_screen/presenter/bloc/home_bloc.dart +++ b/lib/features/home_screen/presenter/bloc/home_bloc.dart @@ -9,7 +9,8 @@ part 'home_event.dart'; part 'home_state.dart'; class HomeBloc extends Bloc { - HomeBloc({required this.hiveHelper, required this.yelpRepository}) : super(HomeInitial()) { + HomeBloc({required this.hiveHelper, required this.yelpRepository}) + : super(HomeInitial()) { on(_onInitialEvent); } @@ -17,9 +18,9 @@ class HomeBloc extends Bloc { final YelpRepository yelpRepository; Future _onInitialEvent( - InitialEvent event, - Emitter emit, - ) async { + InitialEvent event, + Emitter emit, + ) async { final yelpRepo = yelpRepository; final result = await yelpRepo.getRestaurants(); @@ -48,4 +49,4 @@ class HomeBloc extends Bloc { }, ); } -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/bloc/home_event.dart b/lib/features/home_screen/presenter/bloc/home_event.dart index 53db2f6..542d459 100644 --- a/lib/features/home_screen/presenter/bloc/home_event.dart +++ b/lib/features/home_screen/presenter/bloc/home_event.dart @@ -4,7 +4,6 @@ sealed class HomeEvent extends Equatable { const HomeEvent(); } - class InitialEvent extends HomeEvent { const InitialEvent(); @@ -17,4 +16,4 @@ class LoadFavoritesEvent extends HomeEvent { @override List get props => []; -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart index 4fa4f34..cd600a5 100644 --- a/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart @@ -19,15 +19,13 @@ class AllRestaurantBloc extends Bloc { final YelpRepository yelpRepository; Future _onInitEvent( - InitialEvent event, - Emitter emit, - ) async { + InitialEvent event, + Emitter emit, + ) async { emit( const LoadingState(), ); - final yelpRepo = yelpRepository; - final result = await yelpRepo.getRestaurants(); - + final result = await yelpRepository.getRestaurants(); result.when( ok: (data) { if (data.restaurants.isNotEmpty) { @@ -49,4 +47,4 @@ class AllRestaurantBloc extends Bloc { }, ); } -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart index dd5a211..a2c5f23 100644 --- a/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_event.dart @@ -9,4 +9,4 @@ class InitialEvent extends AllRestaurantEvent { @override List get props => []; -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart index a0d3c66..f2ca100 100644 --- a/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_state.dart @@ -39,4 +39,4 @@ class ErrorState extends AllRestaurantState { @override List get props => []; -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart index 56aaaef..a923336 100644 --- a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/all_restaurants_tab.dart @@ -1,4 +1,3 @@ - import 'package:animate_do/animate_do.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -9,7 +8,6 @@ import 'package:restaurant_tour/features/home_screen/presenter/children/all_rest import 'package:restaurant_tour/repositories/yelp_repository.dart'; import 'package:restaurant_tour/shared/widgets/home_loading_skeleton.dart'; - class AllRestaurantsTab extends StatelessWidget { const AllRestaurantsTab({ super.key, @@ -66,11 +64,11 @@ class _Body extends StatelessWidget { final restaurant = state.restaurantList[index]; final int delay = index * 500; return FadeInRight( + delay: Duration(milliseconds: delay), child: CardRestaurant( restaurant: restaurant, isFromFavorites: false, ), - delay: Duration(milliseconds: delay), ); }, ); @@ -85,4 +83,4 @@ class _Body extends StatelessWidget { }, ); } -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart index ff98cf9..295ac06 100644 --- a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart @@ -20,8 +20,11 @@ class CardRestaurant extends StatelessWidget { @override Widget build(BuildContext context) { return InkWell( - onTap: () => onTap(context, - restaurant: restaurant, isFromFavorites: isFromFavorites), + onTap: () => onTap( + context, + restaurant: restaurant, + isFromFavorites: isFromFavorites, + ), child: Padding( padding: const EdgeInsets.symmetric( vertical: 4.0, @@ -122,7 +125,11 @@ class CardRestaurant extends StatelessWidget { required Restaurant restaurant, required bool isFromFavorites, }) { - context.pushNamed('restaurant-page', extra: restaurant,) + context + .pushNamed( + 'restaurant-page', + extra: restaurant, + ) .then( (result) { if (result == true && isFromFavorites) { diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant_export.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant_export.dart index 7867d2f..a11ae94 100644 --- a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant_export.dart +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant_export.dart @@ -1,3 +1,3 @@ export 'card_restaurant.dart'; export 'restaurant_card_skeleton.dart'; -export 'status_indicator.dart'; \ No newline at end of file +export 'status_indicator.dart'; diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/status_indicator.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/status_indicator.dart index e93557e..292fc63 100644 --- a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/status_indicator.dart +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/status_indicator.dart @@ -28,4 +28,4 @@ class StatusIndicator extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart index c300317..2b52956 100644 --- a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/my_favorites_tab.dart @@ -9,4 +9,4 @@ class MyFavoritesTab extends StatelessWidget { Widget build(BuildContext context) { return const Text('data'); } -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/api/favorite_restaurants_api.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/api/favorite_restaurants_api.dart index 6d1e384..658f2dc 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/data/api/favorite_restaurants_api.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/api/favorite_restaurants_api.dart @@ -4,8 +4,6 @@ import 'package:restaurant_tour/core/helpers/dio_helper.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.dart'; - - class FavoriteRestaurantsApi extends FavoriteRestaurantsRepository { final Dio dio; @@ -59,18 +57,22 @@ class FavoriteRestaurantsApi extends FavoriteRestaurantsRepository { final result = RestaurantModel.fromJson(response.data!); return Ok(result); } else { - return Err(DioException( - error: 'La respuesta no contiene datos', - requestOptions: RequestOptions(path: '/v3/graphql'), - )); + return Err( + DioException( + error: 'La respuesta no contiene datos', + requestOptions: RequestOptions(path: '/v3/graphql'), + ), + ); } } on DioException catch (e) { return Err(e); } catch (e) { - return Err(DioException( - error: 'Error desconocido: $e', - requestOptions: RequestOptions(path: '/v3/graphql'), - )); + return Err( + DioException( + error: 'Error desconocido: $e', + requestOptions: RequestOptions(path: '/v3/graphql'), + ), + ); } } -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart index bad0ae2..75c5091 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart @@ -1,25 +1,20 @@ import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart'; -class CategoryModel extends CategoryEntity { - const CategoryModel({ - required String title, - required String alias, - }) : super( - title: title, - alias: alias, - ); +class CategoryModel { + const CategoryModel(this.categoryEntity); + final CategoryEntity categoryEntity; - Map toJson() { - return { - 'title': title, - 'alias': alias, - }; - } + Map toJson() => { + 'title': categoryEntity.title, + 'alias': categoryEntity.alias, + }; factory CategoryModel.fromJson(Map json) { return CategoryModel( - title: json['title'] ?? '', - alias: json['alias'] ?? '', + ( + title: json['title'] ?? '', + alias: json['alias'] ?? '', + ), ); } -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart index a3dee4f..3e7e76a 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart @@ -1,13 +1,12 @@ import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart'; -class HourModel extends HourEntity { - const HourModel({ - required bool isOpenNow, - }) : super( - isOpenNow: isOpenNow, - ); +class HourModel { + const HourModel(this.hourEntity); + + final HourEntity hourEntity; Map toJson() { + var (isOpenNow) = hourEntity; return { 'is_open_now': isOpenNow, }; @@ -15,7 +14,7 @@ class HourModel extends HourEntity { factory HourModel.fromJson(Map json) { return HourModel( - isOpenNow: json['is_open_now'] as bool, + (json['is_open_now'] as bool? ?? false,), ); } -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart index 63645ba..140408d 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart @@ -1,8 +1,7 @@ import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/location_entity.dart'; class LocationModel extends LocationEntity { - const LocationModel({required String formattedAddress}) - : super(formattedAddress: formattedAddress); + const LocationModel({required super.formattedAddress}); factory LocationModel.fromJson(Map json) { return LocationModel( @@ -15,4 +14,4 @@ class LocationModel extends LocationEntity { 'formatted_address': formattedAddress, }; } -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart index 4f9be91..2b50db1 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/restaurant_model.dart @@ -1,5 +1,3 @@ - - import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart'; @@ -50,19 +48,18 @@ class RestaurantModel extends RestaurantEntity { .map((x) => HourModel.fromJson(x as Map)) .toList(); final locationJson = businessJson['location'] as Map?; - final location = LocationModel.fromJson(locationJson!); + final location = LocationModel.fromJson(locationJson!); return RestaurantModel( - id: id, - name: name, - price: price, - rating: rating, - photos: photos, - reviews: reviews, - categories: categories, - hours: hours, - location:location + id: id, + name: name, + price: price, + rating: rating, + photos: photos, + reviews: reviews, + categories: categories, + hours: hours, + location: location, ); } - -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart index d419f21..fdd8413 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart @@ -3,16 +3,11 @@ import 'package:restaurant_tour/features/home_screen/presenter/children/favorite class ReviewModel extends ReviewEntity { const ReviewModel({ - required String id, - required int rating, - required String text, - required UserModel user, - }) : super( - id: id, - rating: rating, - text: text, - user: user, - ); + required super.id, + required super.rating, + required super.text, + required super.user, + }); Map toJson() { return { @@ -31,4 +26,4 @@ class ReviewModel extends ReviewEntity { user: UserModel.fromJson(json['user'] as Map), ); } -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart index 37fca31..508b620 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart @@ -2,14 +2,10 @@ import 'package:restaurant_tour/features/home_screen/presenter/children/favorite class UserModel extends UserEntity { const UserModel({ - required String id, - required String name, - required String imageUrl, - }) : super( - id: id, - name: name, - imageUrl: imageUrl, - ); + required super.id, + required super.name, + required super.imageUrl, + }); factory UserModel.fromJson(Map json) { return UserModel( @@ -26,4 +22,4 @@ class UserModel extends UserEntity { 'image_url': imageUrl, }; } -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart index fc5838b..1a11a76 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart @@ -1,9 +1,4 @@ -class CategoryEntity { - const CategoryEntity({ - required this.title, - required this.alias, - }); - - final String title; - final String alias; -} \ No newline at end of file +typedef CategoryEntity = ({ + String title, + String alias, +}); diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart index 65e4c04..0d04de7 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart @@ -1,7 +1 @@ -class HourEntity { - const HourEntity({ - required this.isOpenNow, - }); - - final bool isOpenNow; -} \ No newline at end of file +typedef HourEntity = (bool isOpenNow,); diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/location_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/location_entity.dart index e0f8fe6..f93c1af 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/location_entity.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/location_entity.dart @@ -4,4 +4,4 @@ class LocationEntity { const LocationEntity({ required this.formattedAddress, }); -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/restaurant_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/restaurant_entity.dart index cadd363..10cdb23 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/restaurant_entity.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/restaurant_entity.dart @@ -25,4 +25,4 @@ class RestaurantEntity { required this.hours, required this.location, }); -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart index 52d0e88..f41dc56 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart @@ -1,4 +1,3 @@ - import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart'; class ReviewEntity { @@ -13,4 +12,4 @@ class ReviewEntity { final int rating; final String text; final UserModel user; -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/user_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/user_entity.dart index adec656..ac634da 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/user_entity.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/user_entity.dart @@ -8,4 +8,4 @@ class UserEntity { required this.imageUrl, required this.name, }); -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.dart index 32d48f3..b73065c 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/repository/favorite_restaurant_respository.dart @@ -6,4 +6,4 @@ abstract class FavoriteRestaurantsRepository { Future> getRestaurantDetails({ required String restaurantId, }); -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart index 3dfd1ae..6f1a9d6 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart @@ -22,9 +22,9 @@ class FavoriteRestaurantsBloc final FavoriteRestaurantsRepository favoriteRestaurantsRepository; Future _onInitialEvent( - InitialEvent event, - Emitter emit, - ) async { + InitialEvent event, + Emitter emit, + ) async { emit(const LoadingState()); final favoriteList = hiveHelper.getAllFavoriteIds(); @@ -53,4 +53,4 @@ class FavoriteRestaurantsBloc emit(FavoriteRestaurantsLoaded(favoriteList: favoriteRestaurants)); } -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart index e314510..89a14a2 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_event.dart @@ -9,4 +9,4 @@ class InitialEvent extends FavoriteRestaurantsEvent { @override List get props => []; -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart index b614395..fcdc120 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_state.dart @@ -37,4 +37,4 @@ class FavErrorState extends FavoriteRestaurantsState { @override List get props => []; -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart index 2927613..37ea642 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/page/favorite_restaurants_tab.dart @@ -8,7 +8,6 @@ import 'package:restaurant_tour/features/home_screen/presenter/children/favorite import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart'; import 'package:restaurant_tour/shared/widgets/home_loading_skeleton.dart'; - class FavoriteRestaurantsTab extends StatelessWidget { const FavoriteRestaurantsTab({super.key}); @@ -19,17 +18,15 @@ class FavoriteRestaurantsTab extends StatelessWidget { hiveHelper: HiveHelper(), favoriteRestaurantsRepository: FavoriteRestaurantsApi(), )..add( - const InitialEvent(), - ), + const InitialEvent(), + ), child: const _Page(), ); } } class _Page extends StatelessWidget { - const _Page({ - super.key, - }); + const _Page(); @override Widget build(BuildContext context) { @@ -50,9 +47,7 @@ class _Page extends StatelessWidget { } class _Body extends StatelessWidget { - const _Body({ - super.key, - }); + const _Body(); @override Widget build(BuildContext context) { @@ -70,11 +65,11 @@ class _Body extends StatelessWidget { final parseRestaurant = Restaurant.fromJson(restaurant.toJson()); final int delay = index * 500; return FadeInRight( + delay: Duration(milliseconds: delay), child: CardRestaurant( - isFromFavorites:true, + isFromFavorites: true, restaurant: parseRestaurant, ), - delay: Duration(milliseconds: delay), ); }, ); @@ -82,7 +77,7 @@ class _Body extends StatelessWidget { if (state is NoFavoritesState) { return const Center( child: Text( - 'No favorite restaurantes were added', + 'No favorite restaurants were added', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w500, @@ -95,4 +90,4 @@ class _Body extends StatelessWidget { }, ); } -} \ No newline at end of file +} diff --git a/lib/features/home_screen/presenter/children/widgets/tab_views.dart b/lib/features/home_screen/presenter/children/widgets/tab_views.dart index 66d824c..be25d95 100644 --- a/lib/features/home_screen/presenter/children/widgets/tab_views.dart +++ b/lib/features/home_screen/presenter/children/widgets/tab_views.dart @@ -18,4 +18,4 @@ class TabViews extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart b/lib/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart index a179942..e0dfc3e 100644 --- a/lib/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart +++ b/lib/features/restaurant_screen/presenter/bloc/restaurant_bloc.dart @@ -16,9 +16,9 @@ class RestaurantBloc extends Bloc { final HiveHelper hiveHelper; Future _onCheckFavoriteEvent( - CheckFavoriteEvent event, - Emitter emit, - ) async { + CheckFavoriteEvent event, + Emitter emit, + ) async { emit(const AppBarLoadingState()); try { List favoriteIds = hiveHelper.getAllFavoriteIds(); @@ -31,9 +31,9 @@ class RestaurantBloc extends Bloc { } Future _onAddFavoriteEvent( - AddFavoriteEvent event, - Emitter emit, - ) async { + AddFavoriteEvent event, + Emitter emit, + ) async { emit(const AppBarLoadingState()); try { await hiveHelper.addFavorite(event.restaurantId); @@ -44,9 +44,9 @@ class RestaurantBloc extends Bloc { } Future _onRemoveFavoriteEvent( - RemoveFavoriteEvent event, - Emitter emit, - ) async { + RemoveFavoriteEvent event, + Emitter emit, + ) async { emit(const AppBarLoadingState()); try { await hiveHelper.removeFavorite(event.restaurantId); @@ -55,4 +55,4 @@ class RestaurantBloc extends Bloc { emit(FavoriteOperationError(message: e.toString())); } } -} \ No newline at end of file +} diff --git a/lib/features/restaurant_screen/presenter/bloc/restaurant_event.dart b/lib/features/restaurant_screen/presenter/bloc/restaurant_event.dart index 7475621..2f6f8e7 100644 --- a/lib/features/restaurant_screen/presenter/bloc/restaurant_event.dart +++ b/lib/features/restaurant_screen/presenter/bloc/restaurant_event.dart @@ -36,4 +36,4 @@ class UpdateListEvent extends RestaurantEvent { @override List get props => []; -} \ No newline at end of file +} diff --git a/lib/features/restaurant_screen/presenter/bloc/restaurant_state.dart b/lib/features/restaurant_screen/presenter/bloc/restaurant_state.dart index 060c998..3401184 100644 --- a/lib/features/restaurant_screen/presenter/bloc/restaurant_state.dart +++ b/lib/features/restaurant_screen/presenter/bloc/restaurant_state.dart @@ -48,4 +48,4 @@ class FavoriteOperationError extends RestaurantState { @override List get props => [message]; -} \ No newline at end of file +} diff --git a/lib/features/restaurant_screen/presenter/page/restaurant_screen.dart b/lib/features/restaurant_screen/presenter/page/restaurant_screen.dart index 94e5841..10844b6 100644 --- a/lib/features/restaurant_screen/presenter/page/restaurant_screen.dart +++ b/lib/features/restaurant_screen/presenter/page/restaurant_screen.dart @@ -23,8 +23,8 @@ class RestaurantScreen extends StatelessWidget { child: Builder( builder: (context) { context.read().add( - CheckFavoriteEvent(restaurant: restaurant), - ); + CheckFavoriteEvent(restaurant: restaurant), + ); return _Page(restaurant: restaurant); }, ), diff --git a/lib/features/restaurant_screen/presenter/page/restaurant_screen_export.dart b/lib/features/restaurant_screen/presenter/page/restaurant_screen_export.dart index 4032a9c..5004235 100644 --- a/lib/features/restaurant_screen/presenter/page/restaurant_screen_export.dart +++ b/lib/features/restaurant_screen/presenter/page/restaurant_screen_export.dart @@ -1,2 +1,2 @@ export 'widgets/widgets_export.dart'; -export 'restaurant_screen.dart'; \ No newline at end of file +export 'restaurant_screen.dart'; diff --git a/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart b/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart index ec8b528..64ac455 100644 --- a/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart +++ b/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart @@ -21,12 +21,26 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { bool isFavorite = false; return AppBar( + backgroundColor: Colors.white, + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title, + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.w700, + color: Colors.black, + ), + ), + ], + ), leading: BackButton( + color: Colors.black, onPressed: () { context.pop(!isFavorite); }, ), - title: Text(title), actions: [ BlocBuilder( builder: (context, state) { @@ -37,7 +51,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { return IconButton( icon: Icon( isFavorite ? Icons.favorite : Icons.favorite_border, - color: isFavorite ? Colors.red : null, + color: isFavorite ? Colors.red : Colors.black, ), onPressed: () { if (!isFavorite) { diff --git a/lib/features/restaurant_screen/presenter/page/widgets/raiting_area.dart b/lib/features/restaurant_screen/presenter/page/widgets/raiting_area.dart index 16b6522..6f11ba0 100644 --- a/lib/features/restaurant_screen/presenter/page/widgets/raiting_area.dart +++ b/lib/features/restaurant_screen/presenter/page/widgets/raiting_area.dart @@ -48,4 +48,4 @@ class RatingArea extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart b/lib/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart index 1ab97af..43d6091 100644 --- a/lib/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart +++ b/lib/features/restaurant_screen/presenter/page/widgets/restaurant_details_area.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant_export.dart'; - - class RestaurantDetailsArea extends StatelessWidget { const RestaurantDetailsArea({super.key, required this.restaurant}); @@ -25,20 +23,20 @@ class RestaurantDetailsArea extends StatelessWidget { ), child: Row( children: [ - Text(restaurant.price.toString() + ', '), + Text('${restaurant.price}, '), Text( restaurant.categories?.first.title ?? '', ), const Spacer(), restaurant.isOpen ? const StatusIndicator( - text: "Open Now", - color: Colors.green, - ) + text: "Open Now", + color: Colors.green, + ) : const StatusIndicator( - text: "Closed", - color: Colors.red, - ), + text: "Closed", + color: Colors.red, + ), ], ), ), @@ -74,4 +72,4 @@ class RestaurantDetailsArea extends StatelessWidget { ], ); } -} \ No newline at end of file +} diff --git a/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart b/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart index be06c6b..a38eb34 100644 --- a/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart +++ b/lib/features/restaurant_screen/presenter/page/widgets/reviews_area.dart @@ -58,12 +58,12 @@ class ReviewsArea extends StatelessWidget { child: CircularProgressIndicator(), ), errorWidget: (context, url, error) => - Image.network( - 'https://fakeimg.pl/600x400', - width: 40, - height: 40, - fit: BoxFit.cover, - ), + Image.network( + 'https://fakeimg.pl/600x400', + width: 40, + height: 40, + fit: BoxFit.cover, + ), ), ), ), diff --git a/lib/features/restaurant_screen/presenter/page/widgets/widgets_export.dart b/lib/features/restaurant_screen/presenter/page/widgets/widgets_export.dart index 6213978..5a04902 100644 --- a/lib/features/restaurant_screen/presenter/page/widgets/widgets_export.dart +++ b/lib/features/restaurant_screen/presenter/page/widgets/widgets_export.dart @@ -1,4 +1,4 @@ export 'custom_app_bar.dart'; export 'raiting_area.dart'; export 'restaurant_details_area.dart'; -export 'reviews_area.dart'; \ No newline at end of file +export 'reviews_area.dart'; diff --git a/lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart b/lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart index ca8bfb0..a71e4e3 100644 --- a/lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_bloc.dart @@ -11,8 +11,11 @@ class SplashScreenBloc extends Bloc { on(_onInitialEvent); } - Future _onInitialEvent(InitialEvent event, Emitter emit) async { - await Future.delayed(const Duration(seconds: 2),); + Future _onInitialEvent( + InitialEvent event, Emitter emit) async { + await Future.delayed( + const Duration(seconds: 2), + ); emit(const PushToHomeState()); } diff --git a/lib/features/splash_screen/presenter/bloc/splash_screen_event.dart b/lib/features/splash_screen/presenter/bloc/splash_screen_event.dart index 688b2f1..699d729 100644 --- a/lib/features/splash_screen/presenter/bloc/splash_screen_event.dart +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_event.dart @@ -9,4 +9,4 @@ class InitialEvent extends SplashScreenEvent { @override List get props => []; -} \ No newline at end of file +} diff --git a/lib/features/splash_screen/presenter/bloc/splash_screen_state.dart b/lib/features/splash_screen/presenter/bloc/splash_screen_state.dart index fd752d5..fe2d336 100644 --- a/lib/features/splash_screen/presenter/bloc/splash_screen_state.dart +++ b/lib/features/splash_screen/presenter/bloc/splash_screen_state.dart @@ -14,4 +14,4 @@ class PushToHomeState extends SplashScreenState { @override List get props => []; -} \ No newline at end of file +} diff --git a/lib/features/splash_screen/presenter/splash_screen.dart b/lib/features/splash_screen/presenter/splash_screen.dart index be310e5..6b5d18d 100644 --- a/lib/features/splash_screen/presenter/splash_screen.dart +++ b/lib/features/splash_screen/presenter/splash_screen.dart @@ -10,15 +10,13 @@ class SplashScreen extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => SplashScreenBloc()..add(const InitialEvent()), - child: _Page(), + child: const _Page(), ); } } class _Page extends StatelessWidget { - const _Page({ - super.key, - }); + const _Page(); @override Widget build(BuildContext context) { @@ -36,9 +34,7 @@ class _Page extends StatelessWidget { } class _Body extends StatelessWidget { - const _Body({ - super.key, - }); + const _Body(); @override Widget build(BuildContext context) { diff --git a/lib/main.dart b/lib/main.dart index 30a2ab5..01cee06 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,4 +20,4 @@ class RestaurantTour extends StatelessWidget { title: 'Restaurant Tour', ); } -} \ No newline at end of file +} diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index b2445a2..8bb25d9 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/repositories/yelp_repository.dart @@ -21,8 +21,9 @@ class YelpRepository { ); } - Future> getRestaurants( - {int offset = 0}) async { + Future> getRestaurants({ + int offset = 0, + }) async { try { final String jsonString = await rootBundle.loadString('assets/restaurants.json'); diff --git a/lib/shared/rate_stars.dart b/lib/shared/rate_stars.dart index 3fcdfda..91d412f 100644 --- a/lib/shared/rate_stars.dart +++ b/lib/shared/rate_stars.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; class RateStars extends StatelessWidget { const RateStars({ - Key? key, + super.key, required this.rate, this.starSize = 20.0, this.color = Colors.amber, - }) : super(key: key); + }); final double rate; final double starSize; @@ -28,4 +28,4 @@ class RateStars extends StatelessWidget { return Row(children: stars); } -} \ No newline at end of file +} diff --git a/lib/shared/widgets/home_loading_skeleton.dart b/lib/shared/widgets/home_loading_skeleton.dart index 7278ba2..23ce2d6 100644 --- a/lib/shared/widgets/home_loading_skeleton.dart +++ b/lib/shared/widgets/home_loading_skeleton.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/restaurant_card_skeleton.dart'; - class CardsLoadingSkeleton extends StatelessWidget { - const CardsLoadingSkeleton({Key? key}) : super(key: key); + const CardsLoadingSkeleton({super.key}); @override Widget build(BuildContext context) { @@ -16,4 +15,4 @@ class CardsLoadingSkeleton extends StatelessWidget { }, ); } -} \ No newline at end of file +} diff --git a/test/lib/features/home_screen/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart b/test/lib/features/home_screen/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart index fc61e1e..2c90edd 100644 --- a/test/lib/features/home_screen/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart +++ b/test/lib/features/home_screen/children/all_restaurant/presenter/bloc/all_restaurant_bloc_test.dart @@ -8,7 +8,6 @@ import 'package:restaurant_tour/core/models/restaurant.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/all_restaurant/presenter/bloc/all_restaurant/all_restaurant_bloc.dart'; import 'package:restaurant_tour/repositories/yelp_repository.dart'; - class MockYelpRepository extends Mock implements YelpRepository {} class MockHiveHelper extends Mock implements HiveHelper {} @@ -65,7 +64,7 @@ var mockRestaurants = [ void main() { group( 'AllRestaurantBloc', - () { + () { late YelpRepository yelpRepository; late AllRestaurantBloc allRestaurantBloc; late HiveHelper hiveHelper; @@ -85,7 +84,7 @@ void main() { 'emits [LoadingState, DataLoadedState] when restaurants are fetched successfully', build: () { when(() => yelpRepository.getRestaurants()).thenAnswer( - (_) async => Result.ok( + (_) async => Result.ok( RestaurantQueryResult( restaurants: mockRestaurants, ), @@ -108,7 +107,7 @@ void main() { final dioError = MockDioException(); when(() => yelpRepository.getRestaurants()).thenAnswer( - (_) async => Result.err(dioError), + (_) async => Result.err(dioError), ); return allRestaurantBloc; }, diff --git a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/category_entity_test.dart b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/category_entity_test.dart index 96c1a12..0fdcfbd 100644 --- a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/category_entity_test.dart +++ b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/category_entity_test.dart @@ -1,11 +1,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart'; - void main() { group( 'CategoryEntity', - () { + () { test('should have the correct properties', () { const title = 'Italian'; const alias = 'italian'; @@ -20,7 +19,7 @@ void main() { test( 'should support value equality', - () { + () { const categoryEntity1 = CategoryEntity( title: 'Italian', alias: 'italian', @@ -41,9 +40,9 @@ void main() { test('should not be equal when properties differ', () { const categoryEntity1 = - CategoryEntity(title: 'Italian', alias: 'italian'); + CategoryEntity(title: 'Italian', alias: 'italian'); const categoryEntity2 = - CategoryEntity(title: 'Mexican', alias: 'mexican'); + CategoryEntity(title: 'Mexican', alias: 'mexican'); expect( categoryEntity1, @@ -56,4 +55,4 @@ void main() { }); }, ); -} \ No newline at end of file +} diff --git a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/hour_entity_test.dart b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/hour_entity_test.dart index 5eb3d01..e2387d4 100644 --- a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/hour_entity_test.dart +++ b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/hour_entity_test.dart @@ -1,11 +1,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart'; - void main() { group( 'HourEntity', - () { + () { test('should have isOpenNow property correctly assigned', () { const isOpenNow = true; const hourEntity = HourEntity(isOpenNow: isOpenNow); @@ -40,4 +39,4 @@ void main() { }); }, ); -} \ No newline at end of file +} diff --git a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart index 9db4a77..1314dc9 100644 --- a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart +++ b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart @@ -8,7 +8,6 @@ import 'package:restaurant_tour/features/home_screen/presenter/children/favorite void main() { group('RestaurantEntity', () { - const id = '1'; const name = 'Test Restaurant'; const price = '\$\$'; @@ -16,13 +15,14 @@ void main() { final photos = ['photo1.jpg', 'photo2.jpg']; final reviews = [ const ReviewModel( - id: 'review1', - user: UserModel(id: 'asdf', name: 'TEST USER', imageUrl: ''), - rating: 5, - text: 'Great!') + id: 'review1', + user: UserModel(id: 'asdf', name: 'TEST USER', imageUrl: ''), + rating: 5, + text: 'Great!', + ), ]; final categories = [ - const CategoryModel(title: 'Italian', alias: 'italian') + const CategoryModel(title: 'Italian', alias: 'italian'), ]; final hours = [const HourModel(isOpenNow: true)]; const location = LocationModel(formattedAddress: '123 Test St'); @@ -51,4 +51,4 @@ void main() { expect(restaurantEntity.location, location); }); }); -} \ No newline at end of file +} diff --git a/test/lib/mock_helpers.dart b/test/lib/mock_helpers.dart index 8aa64af..c384561 100644 --- a/test/lib/mock_helpers.dart +++ b/test/lib/mock_helpers.dart @@ -2,7 +2,6 @@ import 'package:mocktail/mocktail.dart'; import 'package:restaurant_tour/core/helpers/hive_helper.dart'; import 'package:restaurant_tour/repositories/yelp_repository.dart'; - class MockHiveHelper extends Mock implements HiveHelper {} -class MockYelpRepository extends Mock implements YelpRepository {} \ No newline at end of file +class MockYelpRepository extends Mock implements YelpRepository {} From d9528043565d5358bd8552344cdd7f9f0579f700 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 16:15:08 -0400 Subject: [PATCH 29/34] feat: added format to unit tests --- .../data/models/hour_model.dart | 2 +- .../data/models/review_model.dart | 26 +++--- .../data/models/user_model.dart | 2 +- .../domain/entities/review_entity.dart | 19 ++--- .../bloc/favorite_restaurants_bloc.dart | 2 +- .../page/widgets/custom_app_bar.dart | 17 ++-- .../domain/entities/category_entity_test.dart | 83 ++++++++----------- .../domain/entities/hour_entity_test.dart | 43 +++++----- .../entities/restaurant_entity_test.dart | 79 ++++++++---------- 9 files changed, 125 insertions(+), 148 deletions(-) diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart index 3e7e76a..352f459 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart @@ -6,7 +6,7 @@ class HourModel { final HourEntity hourEntity; Map toJson() { - var (isOpenNow) = hourEntity; + final (isOpenNow) = hourEntity; return { 'is_open_now': isOpenNow, }; diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart index fdd8413..993a67e 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart @@ -1,29 +1,31 @@ import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart'; -class ReviewModel extends ReviewEntity { - const ReviewModel({ - required super.id, - required super.rating, - required super.text, - required super.user, - }); +class ReviewModel { + final ReviewEntity reviewEntity; + + const ReviewModel(this.reviewEntity); Map toJson() { + final (id, rating, text, user) = reviewEntity; return { 'id': id, 'rating': rating, 'text': text, - 'user': user.toMap(), + 'user': user.toJson(), }; } factory ReviewModel.fromJson(Map json) { return ReviewModel( - id: json['id'] ?? '', - rating: json['rating'] ?? 0.0, - text: json['text'] ?? '', - user: UserModel.fromJson(json['user'] as Map), + ( + json['id'] as String, + json['rating'] as int, + json['text'] as String, + UserModel.fromJson( + json['user'] as Map, + ), + ), ); } } diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart index 508b620..4785d3c 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart @@ -15,7 +15,7 @@ class UserModel extends UserEntity { ); } - Map toMap() { + Map toJson() { return { 'id': id, 'name': name, diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart index f41dc56..0898fe4 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/review_entity.dart @@ -1,15 +1,8 @@ import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart'; -class ReviewEntity { - const ReviewEntity({ - required this.id, - required this.rating, - required this.text, - required this.user, - }); - - final String id; - final int rating; - final String text; - final UserModel user; -} +typedef ReviewEntity = ( + String id, + int rating, + String text, + UserModel user, +); diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart index 6f1a9d6..595e42a 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/presenter/bloc/favorite_restaurants_bloc.dart @@ -35,7 +35,7 @@ class FavoriteRestaurantsBloc return; } - for (var restaurantId in favoriteList) { + for (String restaurantId in favoriteList) { final response = await favoriteRestaurantsRepository.getRestaurantDetails( restaurantId: restaurantId, ); diff --git a/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart b/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart index 64ac455..d3a4819 100644 --- a/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart +++ b/lib/features/restaurant_screen/presenter/page/widgets/custom_app_bar.dart @@ -25,12 +25,17 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { title: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - title, - textAlign: TextAlign.center, - style: const TextStyle( - fontWeight: FontWeight.w700, - color: Colors.black, + SizedBox( + width: MediaQuery.sizeOf(context).width * 0.65, + child: Text( + title, + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.w700, + color: Colors.black, + ), ), ), ], diff --git a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/category_entity_test.dart b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/category_entity_test.dart index 0fdcfbd..2a3aca8 100644 --- a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/category_entity_test.dart +++ b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/category_entity_test.dart @@ -1,58 +1,41 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/category_entity.dart'; void main() { - group( - 'CategoryEntity', - () { - test('should have the correct properties', () { - const title = 'Italian'; - const alias = 'italian'; - const categoryEntity = CategoryEntity( - title: title, - alias: alias, - ); - - expect(categoryEntity.title, title); - expect(categoryEntity.alias, alias); - }); + group('CategoryEntity', () { + test('should have the correct properties', () { + const categoryEntity = ( + title: 'Italian', + alias: 'italian', + ); - test( - 'should support value equality', - () { - const categoryEntity1 = CategoryEntity( - title: 'Italian', - alias: 'italian', - ); - const categoryEntity2 = CategoryEntity( - title: 'Italian', - alias: 'italian', - ); + expect(categoryEntity.title, 'Italian'); + expect(categoryEntity.alias, 'italian'); + }); - expect( - categoryEntity1, - equals( - categoryEntity2, - ), - ); - }, + test('should support value equality', () { + const categoryEntity1 = ( + title: 'Italian', + alias: 'italian', + ); + const categoryEntity2 = ( + title: 'Italian', + alias: 'italian', ); - test('should not be equal when properties differ', () { - const categoryEntity1 = - CategoryEntity(title: 'Italian', alias: 'italian'); - const categoryEntity2 = - CategoryEntity(title: 'Mexican', alias: 'mexican'); + expect(categoryEntity1, equals(categoryEntity2)); + }); + + test('should not be equal when properties differ', () { + const categoryEntity1 = ( + title: 'Italian', + alias: 'italian', + ); + const categoryEntity2 = ( + title: 'Mexican', + alias: 'mexican', + ); - expect( - categoryEntity1, - isNot( - equals( - categoryEntity2, - ), - ), - ); - }); - }, - ); -} + expect(categoryEntity1, isNot(equals(categoryEntity2))); + }); + }); +} \ No newline at end of file diff --git a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/hour_entity_test.dart b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/hour_entity_test.dart index e2387d4..6026108 100644 --- a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/hour_entity_test.dart +++ b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/hour_entity_test.dart @@ -1,32 +1,35 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart'; void main() { group( 'HourEntity', - () { - test('should have isOpenNow property correctly assigned', () { - const isOpenNow = true; - const hourEntity = HourEntity(isOpenNow: isOpenNow); + () { + test( + 'should have isOpenNow property correctly assigned', + () { + const hourEntity = (isOpenNow: true,); + expect(hourEntity.isOpenNow, true); + }, + ); - expect(hourEntity.isOpenNow, isOpenNow); - }); - - test('should support value equality based on isOpenNow', () { - const hourEntity1 = HourEntity(isOpenNow: true); - const hourEntity2 = HourEntity(isOpenNow: true); + test( + 'should support value equality based on isOpenNow', + () { + const hourEntity1 = (isOpenNow: true,); + const hourEntity2 = (isOpenNow: true,); - expect( - hourEntity1, - equals( - hourEntity2, - ), - ); - }); + expect( + hourEntity1, + equals( + hourEntity2, + ), + ); + }, + ); test('should not be equal when isOpenNow values differ', () { - const hourEntity1 = HourEntity(isOpenNow: true); - const hourEntity2 = HourEntity(isOpenNow: false); + const hourEntity1 = (isOpenNow: true,); + const hourEntity2 = (isOpenNow: false,); expect( hourEntity1, diff --git a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart index 1314dc9..4c74576 100644 --- a/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart +++ b/test/lib/features/home_screen/children/favorite_restaurants/domain/entities/restaurant_entity_test.dart @@ -1,54 +1,45 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/category_model.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/location_model.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/review_model.dart'; -import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/data/models/user_model.dart'; import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/restaurant_entity.dart'; +class MockReviewModel extends Mock implements ReviewModel {} + +class MockCategoryModel extends Mock implements CategoryModel {} + +class MockHourModel extends Mock implements HourModel {} + +class MockLocationModel extends Mock implements LocationModel {} + void main() { - group('RestaurantEntity', () { - const id = '1'; - const name = 'Test Restaurant'; - const price = '\$\$'; - const rating = 4.5; - final photos = ['photo1.jpg', 'photo2.jpg']; - final reviews = [ - const ReviewModel( - id: 'review1', - user: UserModel(id: 'asdf', name: 'TEST USER', imageUrl: ''), - rating: 5, - text: 'Great!', - ), - ]; - final categories = [ - const CategoryModel(title: 'Italian', alias: 'italian'), - ]; - final hours = [const HourModel(isOpenNow: true)]; - const location = LocationModel(formattedAddress: '123 Test St'); + group( + 'RestaurantEntity Test', + () { + test( + 'RestaurantEntity should correctly assign properties', + () { + final mockReviewModel = MockReviewModel(); + final mockCategoryModel = MockCategoryModel(); + final mockHourModel = MockHourModel(); + final mockLocationModel = MockLocationModel(); - test('should correctly assign properties', () { - final restaurantEntity = RestaurantEntity( - id: id, - name: name, - price: price, - rating: rating, - photos: photos, - reviews: reviews, - categories: categories, - hours: hours, - location: location, + final restaurantEntity = RestaurantEntity( + id: '1', + name: 'Test Restaurant', + price: '\$\$', + rating: 4.5, + photos: ['photo1.jpg', 'photo2.jpg'], + reviews: [mockReviewModel], + categories: [mockCategoryModel], + hours: [mockHourModel], + location: mockLocationModel, + ); + expect(restaurantEntity.id, '1'); + }, ); - - expect(restaurantEntity.id, id); - expect(restaurantEntity.name, name); - expect(restaurantEntity.price, price); - expect(restaurantEntity.rating, rating); - expect(restaurantEntity.photos, photos); - expect(restaurantEntity.reviews, reviews); - expect(restaurantEntity.categories, categories); - expect(restaurantEntity.hours, hours); - expect(restaurantEntity.location, location); - }); - }); -} + }, + ); +} \ No newline at end of file From 2015ee683573a9293b556fc11c4bb2e6508b3f64 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 16:17:20 -0400 Subject: [PATCH 30/34] chore: sealed class implementation --- lib/core/models/Failure.dart | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/core/models/Failure.dart b/lib/core/models/Failure.dart index a10bcf4..4bc71c5 100644 --- a/lib/core/models/Failure.dart +++ b/lib/core/models/Failure.dart @@ -6,18 +6,24 @@ abstract class Failure extends Equatable { const Failure({ required this.message, - this.statusCode = 0, + this.statusCode, }); @override - List get props => [message]; + List get props => [ + message, + if (statusCode != null) statusCode, + ]; } class ServerFailure extends Failure { const ServerFailure({ - required super.message, - super.statusCode = null, - }); + required String message, + int? statusCode, + }) : super( + message: message, + statusCode: statusCode, + ); } class LocalFailure extends Failure { From e621a17994b91891fc9cf2371ec2b08e99e40a27 Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 16:23:39 -0400 Subject: [PATCH 31/34] change of typo in hour model typeDef --- .../data/models/hour_model.dart | 13 ++++++------- .../domain/entities/hour_entity.dart | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart index 352f459..5866ee9 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/data/models/hour_model.dart @@ -1,20 +1,19 @@ import 'package:restaurant_tour/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart'; class HourModel { - const HourModel(this.hourEntity); - final HourEntity hourEntity; + const HourModel(this.hourEntity); + Map toJson() { - final (isOpenNow) = hourEntity; + final isOpenNow = hourEntity.$1; return { 'is_open_now': isOpenNow, }; } factory HourModel.fromJson(Map json) { - return HourModel( - (json['is_open_now'] as bool? ?? false,), - ); + HourEntity hourEntity = (json['is_open_now'] as bool? ?? false,); + return HourModel(hourEntity); } -} +} \ No newline at end of file diff --git a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart index 0d04de7..2559510 100644 --- a/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart +++ b/lib/features/home_screen/presenter/children/favorite_restaurants/domain/entities/hour_entity.dart @@ -1 +1 @@ -typedef HourEntity = (bool isOpenNow,); +typedef HourEntity = (bool,); \ No newline at end of file From adbd5a863b5c92b3be1ab0ff83ea41f09957ff4a Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 16:30:40 -0400 Subject: [PATCH 32/34] feat: README update --- README.md | 213 +++++------------------------------------------------- 1 file changed, 19 insertions(+), 194 deletions(-) diff --git a/README.md b/README.md index 412d444..1019ead 100644 --- a/README.md +++ b/README.md @@ -1,202 +1,27 @@ -# Restaurant Tour +# Superformula Mobile Test -Welcome to Superformula's Coding challenge, we are excited to see what you can build! +@paolojoaquinp -This take home test aims to evaluate your skills in building a Flutter application. We are looking for a well-structured and well-tested application that demonstrates your knowledge of Flutter and the Dart language. +## Summary +This initiative embraces Clean Architecture tenets with a Feature-First methodology, crafted for productive and systematic development. It leverages Yelp's GraphQL API for restaurant data retrieval, augmented by a JSON file containing cached information to address Yelp's daily query constraints. Subsequent establishment specifics and patron evaluations are obtained in real-time. -We are not looking for pixel perfect designs, but we are looking for a well-structured application that demonstrates your skills and best practices developing a flutter application. We know there are many ways to solve a problem, and we are interested in seeing how you approach this one. If you have any questions, please don't hesitate to ask. +## Code Structure +The source code is divided into several directories, mirroring the conceptual separation advocated by Clean Architecture guidelines: -Things we'll be looking on your submission: -- App structure for scalability -- Error and optional (?) handling -- Widget tree optimization -- State management -- Test coverage +- `core`: Encompasses fundamental utilities and facilitators such as `dio_helper.dart` for network communication and `hive_helper.dart` for local data retention. +- `models`: Encompasses data structures like `restaurant.dart`. +- `navigation`: Oversees application traversal via files such as `route_navigator.dart`. +- `services`: Facilitates initialization and service configuration through `app_init.dart`. +- `features`: Categorized by individual views/screens, e.g., `home_page` and `restaurant_page`, each feature encapsulating its own business rules, data handling, and UI logic. +- `repositories`: Houses `yelp_repository.dart` for interfacing with the Yelp API. +- `shared`: Accommodates reusable UI components like `single_restaurant_card` and utility elements such as `status_indicator.dart`. -Think of the app you'll be building as the final product, do not over engineer it for possible future features, but do not under engineer it either. We are looking for a balance. We want that the functionalities that you implement are well thought out and implemented. +Furthermore, the project utilizes `dotenv` for environment variable administration, enhancing security and adaptability. -As an example, for the favorites feature you can simply use SharedPreferences, you don't need to use a complex database solution, but we're looking for a solid shared preferences implementation. +## How to install it +Prior to executing the project, establish an `.env` file make a copy from the `.env.example` file in the project's root directory with the following entry: +YELP_API_KEY=thisISanApiKEYYouMUSTAdd - -Be sure to read **all** of this document carefully, and follow the guidelines within. - -## Vendorized Flutter - -3. We use [fvm](https://fvm.app/) for managing the flutter version within the project. Using terminal, while being on the test repository, install the tools dependencies by running the following commands: - - ```sh - dart pub global activate fvm - ``` - - The output of the command will ask to add the folder `./pub-cache/bin` to your PATH variables, if you didn't already. If that is the case, add it to your environment variables, and restart the terminal. - - ```sh - export PATH="$PATH":"$HOME/.pub-cache/bin" # Add this to your environment variables - ``` - -4. Install the project's flutter version using `fvm`. - - ```sh - fvm use - ``` - -5. From now on, you will run all the flutter commands with the `fvm` prefix. Get all the projects dependencies. - - ```sh - fvm flutter pub get - ``` - -More information on the approach can be found here: - -> hhttps://fvm.app/docs/getting_started/installation - -From the root directory: - - -### IDE Setup - -
-Use with VSCode -

- -If you're a VScode user link the new Flutter SDK path in your settings -`$projectRoot/.vscode/settings.json` (create if it doesn't exist yet) - -```json -{ - "dart.flutterSdkPath": ".fvm/flutter_sdk" -} -``` - - -

-
- -
-Use with IntelliJ / Android Studio -

- -Go to `Preferences > Languages & Frameworks > Flutter` and set the Flutter SDK path to `$projectRoot/.fvm/flutter_sdk` - -IntelliJ Settings - -

-
- -## Requirements - -### App Structure - -#### Restaurant List Page - -- Tab Bar - - List of favorites (stored client side) - - List of businesses - - Hero image - - Name - - Price - - Category - - Rating (rounded to the nearest value) - - Open/Closed - -#### Restaurant Detail View - -- Ability to favorite a business -- Name -- Hero image -- Price and category -- Address -- Rating -- Total reviews -- List of reviews - - User name - - Rating - - User image - - Review Text (These are just snippets of the full review, usually like 3-4 lines long) - -#### Misc. - -- Clear documentation on the structure and architecture of your application. -- Clear and logical commit messages. - - We suggest following [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) - -## Test Coverage - -To demonstrate your experience writing different types of tests in Flutter please do the following: - -- We are looking to see how you write tests in Flutter. We are not looking for 100% coverage but we are looking for a good mix of unit and widget tests. -- We are specially looking for you to cover at least one file for each domain layer (interface, application, repositories, etc). - -Feel free to add more tests as you see fit but the above is the minimum requirement. - -## Design - -- See this [Figma File](https://www.figma.com/file/KsEhQUp66m9yeVkvQ0hSZm/Flutter-Test?node-id=0%3A1) for design information related to the overall look and feel of the application. We do not expect pixel-perfection but would like the application to visually be close to what is specified in the Figma file. - -![List View](screenshots/listview.png) -![Detail View](screenshots/detailview.png) - -## API - -The [Yelp GraphQL API](https://www.yelp.com/developers/graphql/guides/intro) is used as the API for this Application. We have provided the boilerplate of the API requests and backing data models to save you some time. To successfully make a request to the Yelp GraphQL API, please follow these steps: - -1. Please go to https://www.yelp.com/signup and sign up for a developer account. -1. Once signed up, navigate to https://www.yelp.com/developers/v3/manage_app. -1. Create a new app by filling out the required information. -1. Once your app is created, scroll down and join the `Developer Beta`. This allows you to use the GraphQL API. -1. Copy your API Key from your app page and paste it on `line 5` [yelp_repository.dart](app/lib/yelp_repository.dart) replacing the `` with your key. -1. Run the app and tap the `Fetch Restaurants` button. If you see a log like `Fetched x restaurants` you are all set! - -## Technical Requirements - -### State Management - -Please restrict your usage of state management or dependency injection to the following options: - -1. [provider](https://pub.dev/packages/provider) -2. [Riverpod](https://pub.dev/packages/riverpod) -3. [bloc](https://pub.dev/packages/bloc) -4. [get_it](https://pub.dev/packages/get_it)/[get_it_mixins](https://pub.dev/packages/get_it_mixin) -5. [Mobx](https://pub.dev/packages/mobx) - -We ask this because this challenge values consistency and efficiency over ingenuity. Using commonly used libraries ensures that we can review your code in a timely manner and allows us to provide better feedback. - -## Coding Values - -At **Superformula** we strive to build applications that have - -- Consistent architecture -- Extensible, clean code -- Solid testing -- Good security & performance best practices - -### Clear, consistent architecture - -Approach your submission as if it were a real world app. This includes Use any libraries that you would normally choose. - -_Please note: we're interested in your code & the way you solve the problem, not how well you can use a particular library or feature._ - -### Easy to understand - -Writing boring code that is easy to follow is essential at **Superformula**. - -We're interested in your method and how you approach the problem just as much as we're interested in the end result. - -### Solid testing approach - -While the purpose of this challenge is not to gauge whether you can achieve 100% test coverage, we do seek to evaluate whether you know how & what to test. - -## Q&A - -> Where should I send back the result when I'm done? - -Please fork this repo and then send us a pull request to our repo when you think you are done. There is no deadline for this task unless otherwise noted to you directly. - -> What if I have a question? - -Just create a new issue in this repo and we will respond and get back to you quickly. - -## Review - -The coding challenge is a take-home test upon which we'll be conducting a thorough code review once complete. The review will consist of meeting some more of our mobile engineers and giving a review of the solution you have designed. Please be prepared to share your screen and run/demo the application to the group. During this process, the engineers will be asking questions. +## Code +This app use `oxidized` for functional programming constructs, fostering a more resilient, fault-tolerant development ecosystem. \ No newline at end of file From d30bf5b78be839e9f84f7c17bc0877921269f68d Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 16:32:14 -0400 Subject: [PATCH 33/34] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1019ead..3d7e3a5 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Furthermore, the project utilizes `dotenv` for environment variable administrati ## How to install it Prior to executing the project, establish an `.env` file make a copy from the `.env.example` file in the project's root directory with the following entry: -YELP_API_KEY=thisISanApiKEYYouMUSTAdd +YELP_API_KEY= ## Code This app use `oxidized` for functional programming constructs, fostering a more resilient, fault-tolerant development ecosystem. \ No newline at end of file From 881ac96ddaca8e970b18be06e9666eedb29a71db Mon Sep 17 00:00:00 2001 From: Paolo Joaquin Pinto Date: Mon, 16 Sep 2024 16:44:10 -0400 Subject: [PATCH 34/34] fixed typo in onPressed event restaurant card --- .../page/widgets/card_restaurant/card_restaurant.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart index 295ac06..2345409 100644 --- a/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart +++ b/lib/features/home_screen/presenter/children/all_restaurant/presenter/page/widgets/card_restaurant/card_restaurant.dart @@ -125,9 +125,8 @@ class CardRestaurant extends StatelessWidget { required Restaurant restaurant, required bool isFromFavorites, }) { - context - .pushNamed( - 'restaurant-page', + context.pushNamed( + 'restaurant-screen', extra: restaurant, ) .then(