From d854666040945961b21c3c985d7cc7a3238efd73 Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Tue, 17 Sep 2024 15:35:25 -0600 Subject: [PATCH 01/15] chore: add dependencies --- .fvmrc | 4 ++++ pubspec.lock | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ pubspec.yaml | 4 ++++ 3 files changed, 72 insertions(+) create mode 100644 .fvmrc diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..e03e940 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.22.3", + "flavors": {} +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index f95a63e..212d41d 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: @@ -177,6 +185,30 @@ 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" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -206,6 +238,22 @@ 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: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" flutter_lints: dependency: "direct dev" description: @@ -371,6 +419,14 @@ 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" package_config: dependency: transitive description: @@ -395,6 +451,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 bc8a205..542f5a6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,8 +11,12 @@ environment: flutter: ">=3.19.6" dependencies: + dio: ^5.7.0 + equatable: ^2.0.5 flutter: sdk: flutter + flutter_bloc: ^8.1.6 + flutter_dotenv: ^5.1.0 http: ^1.2.2 json_annotation: ^4.9.0 From b170ea41d1c9a9e05e397ebd1ec4e3dd8e47ddfa Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Tue, 17 Sep 2024 19:46:45 -0600 Subject: [PATCH 02/15] feat: add and configure dotenv --- .gitignore | 6 +- .vscode/launch.json | 16 ++++- environments/.env.example | 2 + lib/core/routes.dart | 0 lib/core/services/dotenv_service.dart | 25 ++++++++ lib/main.dart | 88 +++++---------------------- pubspec.yaml | 2 + 7 files changed, 64 insertions(+), 75 deletions(-) create mode 100644 environments/.env.example create mode 100644 lib/core/routes.dart create mode 100644 lib/core/services/dotenv_service.dart diff --git a/.gitignore b/.gitignore index 1be2d87..7ff13d6 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,8 @@ app.*.map.json /android/app/release # fvm -.fvm/flutter_sdk \ No newline at end of file +.fvm/flutter_sdk + +# environments +environments/.env.dev +environments/.env.prod diff --git a/.vscode/launch.json b/.vscode/launch.json index 5d0f1d3..b927197 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,9 +5,21 @@ "version": "0.2.0", "configurations": [ { - "name": "app", + "name": "dev", "request": "launch", - "type": "dart" + "type": "dart", + "args": [ + "--dart-define=environment=dev" + ] + }, + { + "name": "prod", + "request": "launch", + "type": "dart", + "args": [ + "--release", + "--dart-define=environment=prod" + ] } ] } \ No newline at end of file diff --git a/environments/.env.example b/environments/.env.example new file mode 100644 index 0000000..d4b0f9c --- /dev/null +++ b/environments/.env.example @@ -0,0 +1,2 @@ +API_KEY=YOUR_API_KEY +API_URL=YOUR_API_URL \ No newline at end of file diff --git a/lib/core/routes.dart b/lib/core/routes.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/core/services/dotenv_service.dart b/lib/core/services/dotenv_service.dart new file mode 100644 index 0000000..f85fce1 --- /dev/null +++ b/lib/core/services/dotenv_service.dart @@ -0,0 +1,25 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class DotenvService { + static final DotenvService _instance = DotenvService._internal(); + + factory DotenvService() => _instance; + + DotenvService._internal(); + + late String _environment; + + Future init() async { + _environment = _getEnvironment(); + await dotenv.load(fileName: 'environments/.env.$_environment'); + } + + String _getEnvironment() => const String.fromEnvironment( + 'environment', + defaultValue: 'dev', + ); + + String get env => _environment; + String get apiKey => dotenv.get('API_KEY'); + String get apiUrl => dotenv.get('API_URL'); +} diff --git a/lib/main.dart b/lib/main.dart index ae7012a..48e94cf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,85 +1,29 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; -import 'package:restaurant_tour/models/restaurant.dart'; -import 'package:restaurant_tour/query.dart'; -const _apiKey = ''; -const _baseUrl = 'https://api.yelp.com/v3/graphql'; +import 'package:restaurant_tour/core/services/dotenv_service.dart'; -void main() { - runApp(const RestaurantTour()); -} +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + final dotenvService = DotenvService(); + await dotenvService.init(); -class RestaurantTour extends StatelessWidget { - const RestaurantTour({super.key}); + print('env: ${dotenvService.env}'); - @override - Widget build(BuildContext context) { - return const MaterialApp( - title: 'Restaurant Tour', - home: HomePage(), - ); - } + runApp( + const MyApp(), + ); } -// 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 $_apiKey', - '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; - } - } +class MyApp extends StatelessWidget { + const MyApp({super.key}); @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'); - } - }, - ), - ], + return const MaterialApp( + title: 'Flutter Test', + home: Scaffold( + body: Center( + child: Text('Env load test'), ), ), ); diff --git a/pubspec.yaml b/pubspec.yaml index 542f5a6..a5f9bef 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,8 @@ dev_dependencies: flutter: generate: true uses-material-design: true + assets: + - environments/ fonts: - family: Lora fonts: From ae4f1446a3d1f8d329174b2f5378814047873010 Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Tue, 17 Sep 2024 22:56:28 -0600 Subject: [PATCH 03/15] feat: add initial design to restaurants list and add rating dependency --- lib/main.dart | 10 +-- lib/modules/home/home_page.dart | 34 +++++++++ .../home/widgets/image_network_loading.dart | 52 +++++++++++++ lib/modules/home/widgets/restaurant_card.dart | 73 +++++++++++++++++++ lib/modules/home/widgets/restaurant_list.dart | 23 ++++++ pubspec.lock | 8 ++ pubspec.yaml | 1 + 7 files changed, 195 insertions(+), 6 deletions(-) create mode 100644 lib/modules/home/home_page.dart create mode 100644 lib/modules/home/widgets/image_network_loading.dart create mode 100644 lib/modules/home/widgets/restaurant_card.dart create mode 100644 lib/modules/home/widgets/restaurant_list.dart diff --git a/lib/main.dart b/lib/main.dart index 48e94cf..3ff3f83 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/routes.dart'; import 'package:restaurant_tour/core/services/dotenv_service.dart'; void main() async { @@ -19,13 +20,10 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( + return MaterialApp( title: 'Flutter Test', - home: Scaffold( - body: Center( - child: Text('Env load test'), - ), - ), + initialRoute: RoutePaths.initial, + routes: getRoutes(), ); } } diff --git a/lib/modules/home/home_page.dart b/lib/modules/home/home_page.dart new file mode 100644 index 0000000..0da4a63 --- /dev/null +++ b/lib/modules/home/home_page.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +import 'package:restaurant_tour/modules/home/widgets/restaurant_list.dart'; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: const Text('Restaurant Tour'), + centerTitle: true, + bottom: const TabBar( + tabs: [ + Tab(text: 'All Restaurants'), + Tab(text: 'My Favorites'), + ], + ), + ), + body: const TabBarView( + children: [ + RestaurantList(), + Center( + child: Text('Tab 2'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/home/widgets/image_network_loading.dart b/lib/modules/home/widgets/image_network_loading.dart new file mode 100644 index 0000000..fc4038a --- /dev/null +++ b/lib/modules/home/widgets/image_network_loading.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +class ImageNetworkLoading extends StatelessWidget { + const ImageNetworkLoading({ + super.key, + this.urlImage, + this.emptyIcon, + this.errorIcon, + this.fit, + this.height, + this.width = 100, + }); + + final String? urlImage; + final Widget? emptyIcon; + final Widget? errorIcon; + final BoxFit? fit; + final double? height; + final double width; + + @override + Widget build(BuildContext context) { + return urlImage == null + ? Icon( + Icons.image, + color: Colors.grey.shade300, + size: width, + ) + : Image.network( + urlImage!, + fit: fit ?? BoxFit.cover, + width: width, + height: height ?? width, + loadingBuilder: (context, child, loadingProgress) => + loadingProgress == null + ? child + : Center( + child: SizedBox( + width: width, + height: height ?? width, + child: const CircularProgressIndicator(), + ), + ), + errorBuilder: (context, error, stackTrace) => + errorIcon ?? + Icon( + Icons.image, + size: width, + ), + ); + } +} diff --git a/lib/modules/home/widgets/restaurant_card.dart b/lib/modules/home/widgets/restaurant_card.dart new file mode 100644 index 0000000..13352b2 --- /dev/null +++ b/lib/modules/home/widgets/restaurant_card.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; + +import 'package:restaurant_tour/modules/home/widgets/image_network_loading.dart'; + +class RestaurantCard extends StatelessWidget { + const RestaurantCard({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.all(2), + margin: const EdgeInsets.all(8), + child: Row( + children: [ + const ImageNetworkLoading(), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Restauran Name'), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('\$500 Italian'), + RatingBar.builder( + initialRating: 3, + minRating: 1, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemSize: 16, + itemPadding: + const EdgeInsets.symmetric(horizontal: 2), + itemBuilder: (context, index) => const Icon( + Icons.star, + color: Colors.amber, + ), + onRatingUpdate: (value) { + print(value); + }, + ), + ], + ), + const Row( + children: [ + Text('Open now'), + Icon( + Icons.circle, + color: Colors.green, + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/home/widgets/restaurant_list.dart b/lib/modules/home/widgets/restaurant_list.dart new file mode 100644 index 0000000..a3c1d90 --- /dev/null +++ b/lib/modules/home/widgets/restaurant_list.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +import 'package:restaurant_tour/core/routes.dart'; +import 'package:restaurant_tour/modules/home/widgets/restaurant_card.dart'; + +class RestaurantList extends StatelessWidget { + const RestaurantList({super.key}); + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: 10, + itemBuilder: (context, index) { + return InkWell( + onTap: () => Navigator.of(context).pushNamed( + RoutePaths.restaurantDetail, + ), + child: const RestaurantCard(), + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 212d41d..a18fb0b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -262,6 +262,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_rating_bar: + dependency: "direct main" + description: + name: flutter_rating_bar + sha256: d2af03469eac832c591a1eba47c91ecc871fe5708e69967073c043b2d775ed93 + url: "https://pub.dev" + source: hosted + version: "4.0.1" flutter_test: dependency: "direct dev" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index a5f9bef..50077f0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: sdk: flutter flutter_bloc: ^8.1.6 flutter_dotenv: ^5.1.0 + flutter_rating_bar: ^4.0.1 http: ^1.2.2 json_annotation: ^4.9.0 From c6c00f9d998ec445b7811683c113c1c61fb5c183 Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Tue, 17 Sep 2024 22:59:04 -0600 Subject: [PATCH 04/15] feat: add initial design to restaurant detail --- lib/core/routes.dart | 16 ++++ .../restaurant_detail_page.dart | 77 +++++++++++++++++++ .../widgets/review_list.dart | 61 +++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 lib/modules/restaurant_detail/restaurant_detail_page.dart create mode 100644 lib/modules/restaurant_detail/widgets/review_list.dart diff --git a/lib/core/routes.dart b/lib/core/routes.dart index e69de29..476544a 100644 --- a/lib/core/routes.dart +++ b/lib/core/routes.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +import 'package:restaurant_tour/modules/home/home_page.dart'; +import 'package:restaurant_tour/modules/restaurant_detail/restaurant_detail_page.dart'; + +class RoutePaths { + static const String initial = '/'; + static const String restaurantDetail = 'restaurant-detail'; +} + +Map getRoutes() { + return { + RoutePaths.initial: (_) => const HomePage(), + RoutePaths.restaurantDetail: (_) => const RestaurantDetailPage(), + }; +} diff --git a/lib/modules/restaurant_detail/restaurant_detail_page.dart b/lib/modules/restaurant_detail/restaurant_detail_page.dart new file mode 100644 index 0000000..2cdfe17 --- /dev/null +++ b/lib/modules/restaurant_detail/restaurant_detail_page.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; + +import 'package:restaurant_tour/modules/home/widgets/image_network_loading.dart'; +import 'package:restaurant_tour/modules/restaurant_detail/widgets/review_list.dart'; + +class RestaurantDetailPage extends StatelessWidget { + const RestaurantDetailPage({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Nombre del restaurante'), + centerTitle: true, + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon( + Icons.favorite, + ), + ), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + Container( + height: MediaQuery.sizeOf(context).height * 0.4, + width: double.infinity, + color: Colors.grey, + child: const ImageNetworkLoading(), + ), + const Padding( + padding: EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('\$500 Italian'), + Row( + children: [ + Text('Open now'), + Icon( + Icons.circle, + color: Colors.green, + ), + ], + ), + ], + ), + SizedBox(height: 16), + Divider(), + SizedBox(height: 16), + Text('Address'), + Text('102 Lakeside Ave'), + Text('Seattle, WA 98122'), + SizedBox(height: 16), + Divider(), + SizedBox(height: 16), + Text('Overral Rating'), + Text('4.6'), + SizedBox(height: 16), + Divider(), + Text('42 reviews'), + ReviewList(), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/restaurant_detail/widgets/review_list.dart b/lib/modules/restaurant_detail/widgets/review_list.dart new file mode 100644 index 0000000..0c408c2 --- /dev/null +++ b/lib/modules/restaurant_detail/widgets/review_list.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; + +class ReviewList extends StatelessWidget { + const ReviewList({super.key}); + + @override + Widget build(BuildContext context) { + return ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => const Divider(), + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + itemCount: 10, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RatingBar.builder( + initialRating: 3, + minRating: 1, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: 5, + itemSize: 16, + itemPadding: const EdgeInsets.symmetric(horizontal: 2), + itemBuilder: (context, index) => const Icon( + Icons.star, + color: Colors.amber, + ), + onRatingUpdate: (value) { + print(value); + }, + ), + const SizedBox(height: 8), + const Text( + 'Const class cannot remove fields: Library:package:restaurant_tour/modules/home/widgets/image_network_loading.dart Class: ImageNetworkLoading. Try performing a hot restart instead.', + ), + const SizedBox(height: 8), + const Row( + children: [ + CircleAvatar( + child: Icon(Icons.person), + ), + Text('User name'), + ], + ), + ], + ), + ); + }, + ); + } +} From f8962bd25860d3a09c031d906c6bdbd6354892ad Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Wed, 18 Sep 2024 00:10:11 -0600 Subject: [PATCH 05/15] design: add all design system for the app --- lib/design_system/components/components.dart | 3 + lib/design_system/components/ds_rating.dart | 44 ++++++++++ lib/design_system/components/ds_texts.dart | 88 +++++++++++++++++++ .../components}/image_network_loading.dart | 0 lib/design_system/design_system.dart | 3 + lib/design_system/foundations/app_colors.dart | 14 +++ lib/design_system/foundations/app_fonts.dart | 11 +++ .../foundations/foundations.dart | 2 + lib/design_system/tokens/ds_colors.dart | 25 ++++++ lib/design_system/tokens/ds_fonts.dart | 9 ++ lib/design_system/tokens/ds_sizes.dart | 21 +++++ lib/design_system/tokens/tokens.dart | 3 + lib/modules/home/widgets/restaurant_card.dart | 2 +- .../restaurant_detail_page.dart | 2 +- 14 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 lib/design_system/components/components.dart create mode 100644 lib/design_system/components/ds_rating.dart create mode 100644 lib/design_system/components/ds_texts.dart rename lib/{modules/home/widgets => design_system/components}/image_network_loading.dart (100%) create mode 100644 lib/design_system/design_system.dart create mode 100644 lib/design_system/foundations/app_colors.dart create mode 100644 lib/design_system/foundations/app_fonts.dart create mode 100644 lib/design_system/foundations/foundations.dart create mode 100644 lib/design_system/tokens/ds_colors.dart create mode 100644 lib/design_system/tokens/ds_fonts.dart create mode 100644 lib/design_system/tokens/ds_sizes.dart create mode 100644 lib/design_system/tokens/tokens.dart diff --git a/lib/design_system/components/components.dart b/lib/design_system/components/components.dart new file mode 100644 index 0000000..616e63c --- /dev/null +++ b/lib/design_system/components/components.dart @@ -0,0 +1,3 @@ +export 'ds_rating.dart'; +export 'ds_texts.dart'; +export 'image_network_loading.dart'; diff --git a/lib/design_system/components/ds_rating.dart b/lib/design_system/components/ds_rating.dart new file mode 100644 index 0000000..6328935 --- /dev/null +++ b/lib/design_system/components/ds_rating.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_rating_bar/flutter_rating_bar.dart'; + +class DsRating extends StatelessWidget { + const DsRating({ + super.key, + required this.initialRating, + required this.itemCount, + this.onRatingUpdate, + this.minRating, + this.itemSize, + this.gapHorizontal, + this.icon, + this.iconColor, + }); + + final double initialRating; + final int itemCount; + final Function(double value)? onRatingUpdate; + final double? minRating; + final double? itemSize; + final double? gapHorizontal; + final IconData? icon; + final Color? iconColor; + + @override + Widget build(BuildContext context) { + return RatingBar.builder( + initialRating: initialRating, + minRating: minRating ?? 1, + direction: Axis.horizontal, + allowHalfRating: true, + itemCount: itemCount, + itemSize: itemSize ?? 16, + itemPadding: EdgeInsets.symmetric(horizontal: gapHorizontal ?? 2), + itemBuilder: (context, index) => Icon( + icon ?? Icons.star, + color: iconColor ?? Colors.amber, + ), + onRatingUpdate: onRatingUpdate ?? (value) => print(value), + ); + } +} diff --git a/lib/design_system/components/ds_texts.dart b/lib/design_system/components/ds_texts.dart new file mode 100644 index 0000000..9dca61d --- /dev/null +++ b/lib/design_system/components/ds_texts.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; + +import 'package:restaurant_tour/design_system/foundations/foundations.dart'; +import 'package:restaurant_tour/design_system/tokens/tokens.dart'; + +enum TextVariant { + /// fontSize: 18 + title(fontSize: 18), + + /// fontSize: 16 + subTitle(fontSize: 16), + + /// fontSize: 14 + medium(fontSize: 14), + + /// fontSize: 12 + caption(fontSize: 12), + + /// fontSize: 10 + small(fontSize: 10); + + const TextVariant({required this.fontSize}); + + final double fontSize; +} + +class DsText extends StatelessWidget { + const DsText({ + super.key, + required this.text, + required this.textVariant, + this.isBold, + this.fontFamily, + this.textAlign, + this.color, + }); + + final String text; + final TextVariant textVariant; + final bool? isBold; + final String? fontFamily; + final TextAlign? textAlign; + final Color? color; + + @override + Widget build(BuildContext context) { + return _BasicText( + text: text, + fontSize: textVariant.fontSize, + fontWeight: isBold == true ? FontWeight.w700 : FontWeight.w400, + color: color ?? DsColors.black, + fontFamily: fontFamily ?? AppFonts.primary, + textAlign: textAlign, + ); + } +} + +class _BasicText extends StatelessWidget { + const _BasicText({ + required this.text, + this.textAlign, + required this.fontSize, + required this.fontWeight, + required this.color, + required this.fontFamily, + }); + + final String text; + final TextAlign? textAlign; + final double fontSize; + final FontWeight fontWeight; + final Color color; + final String fontFamily; + + @override + Widget build(BuildContext context) { + return Text( + text, + textAlign: textAlign, + style: TextStyle( + fontFamily: fontFamily, + fontSize: fontSize, + fontWeight: fontWeight, + color: color, + ), + ); + } +} diff --git a/lib/modules/home/widgets/image_network_loading.dart b/lib/design_system/components/image_network_loading.dart similarity index 100% rename from lib/modules/home/widgets/image_network_loading.dart rename to lib/design_system/components/image_network_loading.dart diff --git a/lib/design_system/design_system.dart b/lib/design_system/design_system.dart new file mode 100644 index 0000000..22c7455 --- /dev/null +++ b/lib/design_system/design_system.dart @@ -0,0 +1,3 @@ +export 'components/components.dart'; +export 'foundations/foundations.dart'; +export 'tokens/tokens.dart'; diff --git a/lib/design_system/foundations/app_colors.dart b/lib/design_system/foundations/app_colors.dart new file mode 100644 index 0000000..74aac6b --- /dev/null +++ b/lib/design_system/foundations/app_colors.dart @@ -0,0 +1,14 @@ +import 'package:restaurant_tour/design_system/tokens/tokens.dart'; + +class AppColors { + AppColors._(); + + /// DsColors.harlequinGreen (#5CD313) + static const success = DsColors.harlequinGreen; + + /// DsColors.fireOpal (#EA5E5E) + static const error = DsColors.fireOpal; + + /// DsColors.americanYellow (#F8B803) + static const stars = DsColors.americanYellow; +} diff --git a/lib/design_system/foundations/app_fonts.dart b/lib/design_system/foundations/app_fonts.dart new file mode 100644 index 0000000..cde8bd6 --- /dev/null +++ b/lib/design_system/foundations/app_fonts.dart @@ -0,0 +1,11 @@ +import 'package:restaurant_tour/design_system/tokens/tokens.dart'; + +class AppFonts { + AppFonts._(); + + /// DsFonts.lora + static const primary = DsFonts.lora; + + /// DsFonts.openSans + static const secundary = DsFonts.openSans; +} diff --git a/lib/design_system/foundations/foundations.dart b/lib/design_system/foundations/foundations.dart new file mode 100644 index 0000000..325fa0b --- /dev/null +++ b/lib/design_system/foundations/foundations.dart @@ -0,0 +1,2 @@ +export 'app_colors.dart'; +export 'app_fonts.dart'; diff --git a/lib/design_system/tokens/ds_colors.dart b/lib/design_system/tokens/ds_colors.dart new file mode 100644 index 0000000..949803f --- /dev/null +++ b/lib/design_system/tokens/ds_colors.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; + +/// the colors names are from https://www.color-name.com + +class DsColors { + DsColors._(); + + /// #5CD313 + static const harlequinGreen = Color(0xFF5CD313); + + /// #EA5E5E + static const fireOpal = Color(0xFFEA5E5E); + + /// #F8B803 + static const americanYellow = Color(0xFFF8B803); + + /// Colors.white + static const white = Colors.white; + + /// Colors.black + static const black = Colors.black; + + /// #D6D6D6 + static const lightSilver = Color(0xFFD6D6D6); +} diff --git a/lib/design_system/tokens/ds_fonts.dart b/lib/design_system/tokens/ds_fonts.dart new file mode 100644 index 0000000..3132b3a --- /dev/null +++ b/lib/design_system/tokens/ds_fonts.dart @@ -0,0 +1,9 @@ +class DsFonts { + DsFonts._(); + + /// lora + static const lora = 'Lora'; + + /// openSans + static const openSans = 'OpenSans'; +} diff --git a/lib/design_system/tokens/ds_sizes.dart b/lib/design_system/tokens/ds_sizes.dart new file mode 100644 index 0000000..6e7bd3e --- /dev/null +++ b/lib/design_system/tokens/ds_sizes.dart @@ -0,0 +1,21 @@ +class DsSizes { + DsSizes._(); + + /// xlg: 24 + static const xlg = 24.0; + + /// md: 18 + static const lg = 18.0; + + /// md: 16 + static const md = 16.0; + + /// sm: 14 + static const sm = 14.0; + + /// xs: 12 + static const xs = 12.0; + + /// xxs: 10 + static const xxs = 10.0; +} diff --git a/lib/design_system/tokens/tokens.dart b/lib/design_system/tokens/tokens.dart new file mode 100644 index 0000000..4717ac5 --- /dev/null +++ b/lib/design_system/tokens/tokens.dart @@ -0,0 +1,3 @@ +export 'ds_colors.dart'; +export 'ds_fonts.dart'; +export 'ds_sizes.dart'; diff --git a/lib/modules/home/widgets/restaurant_card.dart b/lib/modules/home/widgets/restaurant_card.dart index 13352b2..af08b98 100644 --- a/lib/modules/home/widgets/restaurant_card.dart +++ b/lib/modules/home/widgets/restaurant_card.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; -import 'package:restaurant_tour/modules/home/widgets/image_network_loading.dart'; +import 'package:restaurant_tour/design_system/components/image_network_loading.dart'; class RestaurantCard extends StatelessWidget { const RestaurantCard({super.key}); diff --git a/lib/modules/restaurant_detail/restaurant_detail_page.dart b/lib/modules/restaurant_detail/restaurant_detail_page.dart index 2cdfe17..ae2acad 100644 --- a/lib/modules/restaurant_detail/restaurant_detail_page.dart +++ b/lib/modules/restaurant_detail/restaurant_detail_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:restaurant_tour/modules/home/widgets/image_network_loading.dart'; +import 'package:restaurant_tour/design_system/components/image_network_loading.dart'; import 'package:restaurant_tour/modules/restaurant_detail/widgets/review_list.dart'; class RestaurantDetailPage extends StatelessWidget { From 210c66d9151834b235bf0bfa4be9801831f4bb30 Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Wed, 18 Sep 2024 00:49:43 -0600 Subject: [PATCH 06/15] refactor: change initial components for new design_system. style: add new components to design_system --- lib/design_system/atoms/atoms.dart | 3 ++ .../ds_image_network.dart} | 4 +- .../{components => atoms}/ds_rating.dart | 0 .../{components => atoms}/ds_texts.dart | 21 +++++--- lib/design_system/components/components.dart | 5 +- lib/design_system/components/status_open.dart | 32 +++++++++++ lib/design_system/components/user_avatar.dart | 28 ++++++++++ lib/design_system/design_system.dart | 1 + lib/design_system/foundations/app_fonts.dart | 4 +- lib/design_system/tokens/ds_sizes.dart | 6 +++ lib/modules/home/home_page.dart | 8 ++- lib/modules/home/widgets/restaurant_card.dart | 48 ++++++----------- .../restaurant_detail_page.dart | 53 ++++++++++--------- .../widgets/review_list.dart | 39 ++++---------- 14 files changed, 152 insertions(+), 100 deletions(-) create mode 100644 lib/design_system/atoms/atoms.dart rename lib/design_system/{components/image_network_loading.dart => atoms/ds_image_network.dart} (94%) rename lib/design_system/{components => atoms}/ds_rating.dart (100%) rename lib/design_system/{components => atoms}/ds_texts.dart (79%) create mode 100644 lib/design_system/components/status_open.dart create mode 100644 lib/design_system/components/user_avatar.dart diff --git a/lib/design_system/atoms/atoms.dart b/lib/design_system/atoms/atoms.dart new file mode 100644 index 0000000..a07589e --- /dev/null +++ b/lib/design_system/atoms/atoms.dart @@ -0,0 +1,3 @@ +export 'ds_rating.dart'; +export 'ds_texts.dart'; +export 'ds_image_network.dart'; diff --git a/lib/design_system/components/image_network_loading.dart b/lib/design_system/atoms/ds_image_network.dart similarity index 94% rename from lib/design_system/components/image_network_loading.dart rename to lib/design_system/atoms/ds_image_network.dart index fc4038a..7cab54a 100644 --- a/lib/design_system/components/image_network_loading.dart +++ b/lib/design_system/atoms/ds_image_network.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -class ImageNetworkLoading extends StatelessWidget { - const ImageNetworkLoading({ +class DsImageNetwork extends StatelessWidget { + const DsImageNetwork({ super.key, this.urlImage, this.emptyIcon, diff --git a/lib/design_system/components/ds_rating.dart b/lib/design_system/atoms/ds_rating.dart similarity index 100% rename from lib/design_system/components/ds_rating.dart rename to lib/design_system/atoms/ds_rating.dart diff --git a/lib/design_system/components/ds_texts.dart b/lib/design_system/atoms/ds_texts.dart similarity index 79% rename from lib/design_system/components/ds_texts.dart rename to lib/design_system/atoms/ds_texts.dart index 9dca61d..0ab0714 100644 --- a/lib/design_system/components/ds_texts.dart +++ b/lib/design_system/atoms/ds_texts.dart @@ -5,10 +5,10 @@ import 'package:restaurant_tour/design_system/tokens/tokens.dart'; enum TextVariant { /// fontSize: 18 - title(fontSize: 18), + title(fontSize: 24), /// fontSize: 16 - subTitle(fontSize: 16), + subTitle(fontSize: 18), /// fontSize: 14 medium(fontSize: 14), @@ -25,14 +25,16 @@ enum TextVariant { } class DsText extends StatelessWidget { - const DsText({ + const DsText( + this.text, { super.key, - required this.text, - required this.textVariant, + this.textVariant = TextVariant.medium, this.isBold, this.fontFamily, this.textAlign, this.color, + this.fontWeight, + this.fontStyle, }); final String text; @@ -41,16 +43,20 @@ class DsText extends StatelessWidget { final String? fontFamily; final TextAlign? textAlign; final Color? color; + final FontWeight? fontWeight; + final FontStyle? fontStyle; @override Widget build(BuildContext context) { return _BasicText( text: text, fontSize: textVariant.fontSize, - fontWeight: isBold == true ? FontWeight.w700 : FontWeight.w400, + fontWeight: + fontWeight ?? (isBold == true ? FontWeight.w700 : FontWeight.w400), color: color ?? DsColors.black, fontFamily: fontFamily ?? AppFonts.primary, textAlign: textAlign, + fontStyle: fontStyle, ); } } @@ -63,6 +69,7 @@ class _BasicText extends StatelessWidget { required this.fontWeight, required this.color, required this.fontFamily, + this.fontStyle, }); final String text; @@ -71,6 +78,7 @@ class _BasicText extends StatelessWidget { final FontWeight fontWeight; final Color color; final String fontFamily; + final FontStyle? fontStyle; @override Widget build(BuildContext context) { @@ -82,6 +90,7 @@ class _BasicText extends StatelessWidget { fontSize: fontSize, fontWeight: fontWeight, color: color, + fontStyle: fontStyle, ), ); } diff --git a/lib/design_system/components/components.dart b/lib/design_system/components/components.dart index 616e63c..a97262f 100644 --- a/lib/design_system/components/components.dart +++ b/lib/design_system/components/components.dart @@ -1,3 +1,2 @@ -export 'ds_rating.dart'; -export 'ds_texts.dart'; -export 'image_network_loading.dart'; +export 'status_open.dart'; +export 'user_avatar.dart'; diff --git a/lib/design_system/components/status_open.dart b/lib/design_system/components/status_open.dart new file mode 100644 index 0000000..c134086 --- /dev/null +++ b/lib/design_system/components/status_open.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +import 'package:restaurant_tour/design_system/atoms/atoms.dart'; +import 'package:restaurant_tour/design_system/foundations/foundations.dart'; +import 'package:restaurant_tour/design_system/tokens/tokens.dart'; + +class StatusOpen extends StatelessWidget { + const StatusOpen({ + super.key, + required this.isOpenNow, + }); + + final bool isOpenNow; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + DsText( + isOpenNow ? 'Open now' : 'Closed', + fontStyle: FontStyle.italic, + ), + const SizedBox(width: DsSizes.xxs), + Icon( + Icons.circle, + size: DsSizes.md, + color: isOpenNow ? AppColors.success : AppColors.error, + ), + ], + ); + } +} diff --git a/lib/design_system/components/user_avatar.dart b/lib/design_system/components/user_avatar.dart new file mode 100644 index 0000000..5664bc8 --- /dev/null +++ b/lib/design_system/components/user_avatar.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +import 'package:restaurant_tour/design_system/atoms/atoms.dart'; +import 'package:restaurant_tour/design_system/tokens/tokens.dart'; + +class UserAvatar extends StatelessWidget { + const UserAvatar({ + super.key, + required this.name, + this.urlImage, + }); + + final String name; + final String? urlImage; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const CircleAvatar( + child: Icon(Icons.person), + ), + const SizedBox(width: DsSizes.xxs), + DsText(name), + ], + ); + } +} diff --git a/lib/design_system/design_system.dart b/lib/design_system/design_system.dart index 22c7455..f60ab31 100644 --- a/lib/design_system/design_system.dart +++ b/lib/design_system/design_system.dart @@ -1,3 +1,4 @@ +export 'atoms/atoms.dart'; export 'components/components.dart'; export 'foundations/foundations.dart'; export 'tokens/tokens.dart'; diff --git a/lib/design_system/foundations/app_fonts.dart b/lib/design_system/foundations/app_fonts.dart index cde8bd6..10a0cc3 100644 --- a/lib/design_system/foundations/app_fonts.dart +++ b/lib/design_system/foundations/app_fonts.dart @@ -4,8 +4,8 @@ class AppFonts { AppFonts._(); /// DsFonts.lora - static const primary = DsFonts.lora; + static const primary = DsFonts.openSans; /// DsFonts.openSans - static const secundary = DsFonts.openSans; + static const secundary = DsFonts.lora; } diff --git a/lib/design_system/tokens/ds_sizes.dart b/lib/design_system/tokens/ds_sizes.dart index 6e7bd3e..66926aa 100644 --- a/lib/design_system/tokens/ds_sizes.dart +++ b/lib/design_system/tokens/ds_sizes.dart @@ -18,4 +18,10 @@ class DsSizes { /// xxs: 10 static const xxs = 10.0; + + /// xxxs: 8 + static const xxxs = 8.0; + + /// min: 4 + static const min = 4.0; } diff --git a/lib/modules/home/home_page.dart b/lib/modules/home/home_page.dart index 0da4a63..f36b53c 100644 --- a/lib/modules/home/home_page.dart +++ b/lib/modules/home/home_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:restaurant_tour/design_system/design_system.dart'; import 'package:restaurant_tour/modules/home/widgets/restaurant_list.dart'; class HomePage extends StatelessWidget { @@ -11,7 +12,12 @@ class HomePage extends StatelessWidget { length: 2, child: Scaffold( appBar: AppBar( - title: const Text('Restaurant Tour'), + title: const DsText( + 'Restaurant Tour', + textVariant: TextVariant.title, + isBold: true, + fontFamily: AppFonts.secundary, + ), centerTitle: true, bottom: const TabBar( tabs: [ diff --git a/lib/modules/home/widgets/restaurant_card.dart b/lib/modules/home/widgets/restaurant_card.dart index af08b98..f174ab1 100644 --- a/lib/modules/home/widgets/restaurant_card.dart +++ b/lib/modules/home/widgets/restaurant_card.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; - -import 'package:restaurant_tour/design_system/components/image_network_loading.dart'; +import 'package:restaurant_tour/design_system/design_system.dart'; class RestaurantCard extends StatelessWidget { const RestaurantCard({super.key}); @@ -16,51 +14,37 @@ class RestaurantCard extends StatelessWidget { ), padding: const EdgeInsets.all(2), margin: const EdgeInsets.all(8), - child: Row( + child: const Row( children: [ - const ImageNetworkLoading(), + DsImageNetwork(), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text('Restauran Name'), - const SizedBox(height: 16), + DsText( + 'Restauran Name', + textVariant: TextVariant.subTitle, + isBold: true, + fontFamily: AppFonts.secundary, + ), + SizedBox(height: DsSizes.md), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text('\$500 Italian'), - RatingBar.builder( - initialRating: 3, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, - itemCount: 5, - itemSize: 16, - itemPadding: - const EdgeInsets.symmetric(horizontal: 2), - itemBuilder: (context, index) => const Icon( - Icons.star, - color: Colors.amber, - ), - onRatingUpdate: (value) { - print(value); - }, + DsText( + '\$500 Italian', ), - ], - ), - const Row( - children: [ - Text('Open now'), - Icon( - Icons.circle, - color: Colors.green, + DsRating( + initialRating: 4, + itemCount: 5, ), ], ), + StatusOpen(isOpenNow: true), ], ), ], diff --git a/lib/modules/restaurant_detail/restaurant_detail_page.dart b/lib/modules/restaurant_detail/restaurant_detail_page.dart index ae2acad..b031ba3 100644 --- a/lib/modules/restaurant_detail/restaurant_detail_page.dart +++ b/lib/modules/restaurant_detail/restaurant_detail_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:restaurant_tour/design_system/components/image_network_loading.dart'; +import 'package:restaurant_tour/design_system/design_system.dart'; import 'package:restaurant_tour/modules/restaurant_detail/widgets/review_list.dart'; class RestaurantDetailPage extends StatelessWidget { @@ -10,7 +10,11 @@ class RestaurantDetailPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Nombre del restaurante'), + title: const DsText( + 'Nombre del restaurante', + textVariant: TextVariant.subTitle, + isBold: true, + ), centerTitle: true, actions: [ IconButton( @@ -28,10 +32,10 @@ class RestaurantDetailPage extends StatelessWidget { height: MediaQuery.sizeOf(context).height * 0.4, width: double.infinity, color: Colors.grey, - child: const ImageNetworkLoading(), + child: const DsImageNetwork(), ), const Padding( - padding: EdgeInsets.all(16.0), + padding: EdgeInsets.all(DsSizes.md), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -39,32 +43,31 @@ class RestaurantDetailPage extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('\$500 Italian'), - Row( - children: [ - Text('Open now'), - Icon( - Icons.circle, - color: Colors.green, - ), - ], - ), + DsText('\$500 Italian'), + StatusOpen(isOpenNow: true), ], ), - SizedBox(height: 16), + SizedBox(height: DsSizes.md), Divider(), - SizedBox(height: 16), - Text('Address'), - Text('102 Lakeside Ave'), - Text('Seattle, WA 98122'), - SizedBox(height: 16), + SizedBox(height: DsSizes.md), + DsText('Address'), + DsText( + '102 Lakeside Ave', + isBold: true, + ), + DsText('Seattle, WA 98122', isBold: true), + SizedBox(height: DsSizes.md), Divider(), - SizedBox(height: 16), - Text('Overral Rating'), - Text('4.6'), - SizedBox(height: 16), + SizedBox(height: DsSizes.md), + DsText('Overral Rating'), + DsText( + '4.6', + textVariant: TextVariant.title, + isBold: true, + ), + SizedBox(height: DsSizes.md), Divider(), - Text('42 reviews'), + DsText('42 reviews'), ReviewList(), ], ), diff --git a/lib/modules/restaurant_detail/widgets/review_list.dart b/lib/modules/restaurant_detail/widgets/review_list.dart index 0c408c2..514a4a2 100644 --- a/lib/modules/restaurant_detail/widgets/review_list.dart +++ b/lib/modules/restaurant_detail/widgets/review_list.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_rating_bar/flutter_rating_bar.dart'; +import 'package:restaurant_tour/design_system/design_system.dart'; class ReviewList extends StatelessWidget { const ReviewList({super.key}); @@ -12,46 +12,27 @@ class ReviewList extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), separatorBuilder: (context, index) => const Divider(), padding: const EdgeInsets.symmetric( - vertical: 16, + vertical: DsSizes.md, ), itemCount: 10, itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, + return const Padding( + padding: EdgeInsets.symmetric( + vertical: DsSizes.xxxs, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - RatingBar.builder( + DsRating( initialRating: 3, - minRating: 1, - direction: Axis.horizontal, - allowHalfRating: true, itemCount: 5, - itemSize: 16, - itemPadding: const EdgeInsets.symmetric(horizontal: 2), - itemBuilder: (context, index) => const Icon( - Icons.star, - color: Colors.amber, - ), - onRatingUpdate: (value) { - print(value); - }, ), - const SizedBox(height: 8), - const Text( + SizedBox(height: DsSizes.xxxs), + DsText( 'Const class cannot remove fields: Library:package:restaurant_tour/modules/home/widgets/image_network_loading.dart Class: ImageNetworkLoading. Try performing a hot restart instead.', ), - const SizedBox(height: 8), - const Row( - children: [ - CircleAvatar( - child: Icon(Icons.person), - ), - Text('User name'), - ], - ), + SizedBox(height: DsSizes.xxxs), + UserAvatar(name: 'User Name test'), ], ), ); From aeb9586b531bb6490881bc24ef2187c76a7445c3 Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Wed, 18 Sep 2024 02:27:43 -0600 Subject: [PATCH 07/15] design: add snackbar extensions and style image widget --- lib/design_system/atoms/ds_image_network.dart | 48 +++++++++++-------- lib/design_system/atoms/ds_rating.dart | 2 +- lib/design_system/design_system.dart | 1 + .../extensions/ds_snack_bar.dart | 48 +++++++++++++++++++ lib/design_system/extensions/extensions.dart | 1 + 5 files changed, 78 insertions(+), 22 deletions(-) create mode 100644 lib/design_system/extensions/ds_snack_bar.dart create mode 100644 lib/design_system/extensions/extensions.dart diff --git a/lib/design_system/atoms/ds_image_network.dart b/lib/design_system/atoms/ds_image_network.dart index 7cab54a..c4afb7a 100644 --- a/lib/design_system/atoms/ds_image_network.dart +++ b/lib/design_system/atoms/ds_image_network.dart @@ -26,27 +26,33 @@ class DsImageNetwork extends StatelessWidget { color: Colors.grey.shade300, size: width, ) - : Image.network( - urlImage!, - fit: fit ?? BoxFit.cover, - width: width, - height: height ?? width, - loadingBuilder: (context, child, loadingProgress) => - loadingProgress == null - ? child - : Center( - child: SizedBox( - width: width, - height: height ?? width, - child: const CircularProgressIndicator(), - ), - ), - errorBuilder: (context, error, stackTrace) => - errorIcon ?? - Icon( - Icons.image, - size: width, - ), + : Padding( + padding: const EdgeInsets.all(16.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.network( + urlImage!, + fit: fit ?? BoxFit.fill, + width: width, + height: height ?? width, + loadingBuilder: (context, child, loadingProgress) => + loadingProgress == null + ? child + : Center( + child: SizedBox( + width: width, + height: height ?? width, + child: const CircularProgressIndicator(), + ), + ), + errorBuilder: (context, error, stackTrace) => + errorIcon ?? + Icon( + Icons.image, + size: width, + ), + ), + ), ); } } diff --git a/lib/design_system/atoms/ds_rating.dart b/lib/design_system/atoms/ds_rating.dart index 6328935..0f2746e 100644 --- a/lib/design_system/atoms/ds_rating.dart +++ b/lib/design_system/atoms/ds_rating.dart @@ -28,7 +28,7 @@ class DsRating extends StatelessWidget { Widget build(BuildContext context) { return RatingBar.builder( initialRating: initialRating, - minRating: minRating ?? 1, + minRating: minRating ?? 0, direction: Axis.horizontal, allowHalfRating: true, itemCount: itemCount, diff --git a/lib/design_system/design_system.dart b/lib/design_system/design_system.dart index f60ab31..9660a33 100644 --- a/lib/design_system/design_system.dart +++ b/lib/design_system/design_system.dart @@ -1,4 +1,5 @@ export 'atoms/atoms.dart'; export 'components/components.dart'; +export 'extensions/extensions.dart'; export 'foundations/foundations.dart'; export 'tokens/tokens.dart'; diff --git a/lib/design_system/extensions/ds_snack_bar.dart b/lib/design_system/extensions/ds_snack_bar.dart new file mode 100644 index 0000000..63202bf --- /dev/null +++ b/lib/design_system/extensions/ds_snack_bar.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +import 'package:restaurant_tour/design_system/design_system.dart'; + +enum SnackBarType { + success( + backgroundColor: AppColors.success, + textColor: DsColors.white, + ), + error( + backgroundColor: AppColors.error, + textColor: DsColors.white, + ); + + const SnackBarType({ + required this.backgroundColor, + required this.textColor, + }); + + final Color backgroundColor; + final Color textColor; +} + +extension SnackBarExtension on BuildContext { + void showDsSnackBar({ + required String message, + required SnackBarType type, + bool? showCloseIcon, + SnackBarBehavior? behavior, + Duration? duration, + }) { + ScaffoldMessenger.of(this).showSnackBar( + SnackBar( + showCloseIcon: showCloseIcon, + closeIconColor: DsColors.white, + behavior: behavior ?? SnackBarBehavior.fixed, + backgroundColor: type.backgroundColor, + duration: duration ?? const Duration(milliseconds: 4000), + content: Text( + message, + style: TextStyle( + color: type.textColor, + ), + ), + ), + ); + } +} diff --git a/lib/design_system/extensions/extensions.dart b/lib/design_system/extensions/extensions.dart new file mode 100644 index 0000000..7da0213 --- /dev/null +++ b/lib/design_system/extensions/extensions.dart @@ -0,0 +1 @@ +export 'ds_snack_bar.dart'; From 839b136d0eed982835af60e01f07af1d8539cba0 Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Wed, 18 Sep 2024 02:28:38 -0600 Subject: [PATCH 08/15] feat: add home bloc to load restaurants --- lib/modules/home/bloc/home_bloc.dart | 42 +++++++++ lib/modules/home/bloc/home_event.dart | 12 +++ lib/modules/home/bloc/home_state.dart | 39 +++++++++ lib/modules/home/home_page.dart | 85 +++++++++++++------ lib/modules/home/widgets/restaurant_card.dart | 24 ++++-- lib/modules/home/widgets/restaurant_list.dart | 23 ++++- 6 files changed, 189 insertions(+), 36 deletions(-) create mode 100644 lib/modules/home/bloc/home_bloc.dart create mode 100644 lib/modules/home/bloc/home_event.dart create mode 100644 lib/modules/home/bloc/home_state.dart diff --git a/lib/modules/home/bloc/home_bloc.dart b/lib/modules/home/bloc/home_bloc.dart new file mode 100644 index 0000000..75908db --- /dev/null +++ b/lib/modules/home/bloc/home_bloc.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/modules/home/repository/home_repository.dart'; + +part 'home_event.dart'; +part 'home_state.dart'; + +class HomeBloc extends Bloc { + HomeBloc({required this.homeRepository}) + : super( + const HomeInitialState(Model()), + ) { + on(_onLoadRestaurantsEvent); + } + + final HomeRepository homeRepository; + + _onLoadRestaurantsEvent( + LoadRestaurantsEvent event, + Emitter emit, + ) async { + emit(LoadingRestaurantsState(state.model)); + try { + final restaurants = await homeRepository.getRestaurants(); + emit( + LoadedRestaurantsState( + state.model.copyWith( + restaurants: restaurants, + ), + ), + ); + } catch (e, stacktrace) { + debugPrint(e.toString()); + debugPrint(stacktrace.toString()); + emit(ErrorLoadRestaurantsState(state.model)); + } + } +} diff --git a/lib/modules/home/bloc/home_event.dart b/lib/modules/home/bloc/home_event.dart new file mode 100644 index 0000000..22d33ce --- /dev/null +++ b/lib/modules/home/bloc/home_event.dart @@ -0,0 +1,12 @@ +part of 'home_bloc.dart'; + +sealed class HomeEvent extends Equatable { + const HomeEvent(); + + @override + List get props => []; +} + +class LoadRestaurantsEvent extends HomeEvent { + const LoadRestaurantsEvent(); +} diff --git a/lib/modules/home/bloc/home_state.dart b/lib/modules/home/bloc/home_state.dart new file mode 100644 index 0000000..3fa367d --- /dev/null +++ b/lib/modules/home/bloc/home_state.dart @@ -0,0 +1,39 @@ +part of 'home_bloc.dart'; + +sealed class HomeState extends Equatable { + final Model model; + + const HomeState(this.model); + + @override + List get props => [model]; +} + +final class HomeInitialState extends HomeState { + const HomeInitialState(super.model); +} + +final class LoadingRestaurantsState extends HomeState { + const LoadingRestaurantsState(super.model); +} + +final class LoadedRestaurantsState extends HomeState { + const LoadedRestaurantsState(super.model); +} + +final class ErrorLoadRestaurantsState extends HomeState { + const ErrorLoadRestaurantsState(super.model); +} + +class Model extends Equatable { + final List? restaurants; + + const Model({this.restaurants}); + + Model copyWith({List? restaurants}) => Model( + restaurants: restaurants ?? this.restaurants, + ); + + @override + List get props => [restaurants]; +} diff --git a/lib/modules/home/home_page.dart b/lib/modules/home/home_page.dart index f36b53c..b86cb5f 100644 --- a/lib/modules/home/home_page.dart +++ b/lib/modules/home/home_page.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + import 'package:restaurant_tour/design_system/design_system.dart'; +import 'package:restaurant_tour/modules/home/bloc/home_bloc.dart'; +import 'package:restaurant_tour/modules/home/repository/home_repository.dart'; import 'package:restaurant_tour/modules/home/widgets/restaurant_list.dart'; class HomePage extends StatelessWidget { @@ -8,33 +12,64 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return DefaultTabController( - length: 2, - child: Scaffold( - appBar: AppBar( - title: const DsText( - 'Restaurant Tour', - textVariant: TextVariant.title, - isBold: true, - fontFamily: AppFonts.secundary, - ), - centerTitle: true, - bottom: const TabBar( - tabs: [ - Tab(text: 'All Restaurants'), - Tab(text: 'My Favorites'), - ], - ), + return BlocProvider( + create: (context) => HomeBloc( + homeRepository: HomeRepository(), + )..add( + const LoadRestaurantsEvent(), ), - body: const TabBarView( - children: [ - RestaurantList(), - Center( - child: Text('Tab 2'), + child: const _Content(), + ); + } +} + +class _Content extends StatelessWidget { + const _Content(); + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state is ErrorLoadRestaurantsState) { + context.showDsSnackBar( + message: 'Sorry! Error when load restaurants', + type: SnackBarType.error, + ); + } + }, + builder: (context, state) { + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + title: const DsText( + 'Restaurant Tour', + textVariant: TextVariant.title, + isBold: true, + fontFamily: AppFonts.secundary, + ), + centerTitle: true, + bottom: const TabBar( + tabs: [ + Tab(text: 'All Restaurants'), + Tab(text: 'My Favorites'), + ], + ), ), - ], - ), - ), + body: TabBarView( + children: [ + RestaurantList( + loading: state is LoadingRestaurantsState, + restaurants: state.model.restaurants ?? [], + ), + const Center( + child: Text('Tab 2'), + ), + ], + ), + ), + ); + }, ); } } diff --git a/lib/modules/home/widgets/restaurant_card.dart b/lib/modules/home/widgets/restaurant_card.dart index f174ab1..3d7c923 100644 --- a/lib/modules/home/widgets/restaurant_card.dart +++ b/lib/modules/home/widgets/restaurant_card.dart @@ -1,9 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; import 'package:restaurant_tour/design_system/design_system.dart'; class RestaurantCard extends StatelessWidget { - const RestaurantCard({super.key}); + const RestaurantCard({ + super.key, + required this.restaurant, + }); + + final Restaurant restaurant; @override Widget build(BuildContext context) { @@ -14,21 +20,23 @@ class RestaurantCard extends StatelessWidget { ), padding: const EdgeInsets.all(2), margin: const EdgeInsets.all(8), - child: const Row( + child: Row( children: [ - DsImageNetwork(), + DsImageNetwork( + urlImage: restaurant.heroImage, + ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ DsText( - 'Restauran Name', + restaurant.name ?? 'No name', textVariant: TextVariant.subTitle, isBold: true, fontFamily: AppFonts.secundary, ), - SizedBox(height: DsSizes.md), + const SizedBox(height: DsSizes.md), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -36,15 +44,15 @@ class RestaurantCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ DsText( - '\$500 Italian', + restaurant.price ?? '---', ), DsRating( - initialRating: 4, + initialRating: restaurant.rating ?? 0, itemCount: 5, ), ], ), - StatusOpen(isOpenNow: true), + StatusOpen(isOpenNow: restaurant.isOpen), ], ), ], diff --git a/lib/modules/home/widgets/restaurant_list.dart b/lib/modules/home/widgets/restaurant_list.dart index a3c1d90..70f73f3 100644 --- a/lib/modules/home/widgets/restaurant_list.dart +++ b/lib/modules/home/widgets/restaurant_list.dart @@ -1,21 +1,38 @@ import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; import 'package:restaurant_tour/core/routes.dart'; import 'package:restaurant_tour/modules/home/widgets/restaurant_card.dart'; class RestaurantList extends StatelessWidget { - const RestaurantList({super.key}); + const RestaurantList({ + super.key, + required this.loading, + required this.restaurants, + }); + + final bool loading; + final List restaurants; @override Widget build(BuildContext context) { + if (loading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return ListView.builder( - itemCount: 10, + itemCount: restaurants.length, itemBuilder: (context, index) { + final currentRestaurant = restaurants[index]; return InkWell( onTap: () => Navigator.of(context).pushNamed( RoutePaths.restaurantDetail, ), - child: const RestaurantCard(), + child: RestaurantCard( + restaurant: currentRestaurant, + ), ); }, ); From 4cf8e97f15a806d36ed1cafe74a94908a9906cbb Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Wed, 18 Sep 2024 02:29:09 -0600 Subject: [PATCH 09/15] add repository and consume query restaurants --- lib/{ => core}/models/restaurant.dart | 0 lib/{ => core}/models/restaurant.g.dart | 0 lib/core/services/dio_service.dart | 64 +++++++++++++++++++ lib/core/services/dotenv_service.dart | 5 +- lib/main.dart | 10 ++- .../home/repository/home_repository.dart | 23 +++++++ lib/{ => modules/home/repository}/query.dart | 0 7 files changed, 97 insertions(+), 5 deletions(-) rename lib/{ => core}/models/restaurant.dart (100%) rename lib/{ => core}/models/restaurant.g.dart (100%) create mode 100644 lib/core/services/dio_service.dart create mode 100644 lib/modules/home/repository/home_repository.dart rename lib/{ => modules/home/repository}/query.dart (100%) 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/core/services/dio_service.dart b/lib/core/services/dio_service.dart new file mode 100644 index 0000000..a70ed3f --- /dev/null +++ b/lib/core/services/dio_service.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +import 'package:dio/dio.dart'; + +import 'package:restaurant_tour/core/services/dotenv_service.dart'; +import 'package:restaurant_tour/design_system/design_system.dart'; + +late Dio dio; + +class DioService { + static final BaseOptions dioOptions = BaseOptions( + baseUrl: DotenvService.apiUrl, + ); + + static init({ + required String apiKey, + required GlobalKey snackbarKey, + }) { + dio = Dio(dioOptions); + + dio.interceptors.add( + LogInterceptor( + requestBody: true, + responseBody: true, + ), + ); + + dio.interceptors.add( + InterceptorsWrapper( + onRequest: (options, handler) { + options.headers['Authorization'] = 'Bearer $apiKey'; + return handler.next(options); + }, + onResponse: (response, handler) => handler.next(response), + onError: (error, handler) async { + if (error.response?.statusCode == 401) { + showSnackBar( + snackbarKey: snackbarKey, + message: 'Invalid credentials', + ); + } + + return handler.next(error); + }, + ), + ); + } + + static showSnackBar({ + required GlobalKey snackbarKey, + required String message, + }) { + final snackBar = SnackBar( + backgroundColor: AppColors.error, + content: Text( + message, + style: const TextStyle( + color: DsColors.white, + ), + ), + ); + snackbarKey.currentState?.showSnackBar(snackBar); + } +} diff --git a/lib/core/services/dotenv_service.dart b/lib/core/services/dotenv_service.dart index f85fce1..e0b34bf 100644 --- a/lib/core/services/dotenv_service.dart +++ b/lib/core/services/dotenv_service.dart @@ -19,7 +19,6 @@ class DotenvService { defaultValue: 'dev', ); - String get env => _environment; - String get apiKey => dotenv.get('API_KEY'); - String get apiUrl => dotenv.get('API_URL'); + static final apiKey = dotenv.get('API_KEY'); + static final apiUrl = dotenv.get('API_URL'); } diff --git a/lib/main.dart b/lib/main.dart index 3ff3f83..0f47b90 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,14 +1,20 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/routes.dart'; +import 'package:restaurant_tour/core/services/dio_service.dart'; import 'package:restaurant_tour/core/services/dotenv_service.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + final snackbarKey = GlobalKey(); + // dotenv final dotenvService = DotenvService(); await dotenvService.init(); - - print('env: ${dotenvService.env}'); + // dio + DioService.init( + apiKey: DotenvService.apiKey, + snackbarKey: snackbarKey, + ); runApp( const MyApp(), diff --git a/lib/modules/home/repository/home_repository.dart b/lib/modules/home/repository/home_repository.dart new file mode 100644 index 0000000..b5b4a15 --- /dev/null +++ b/lib/modules/home/repository/home_repository.dart @@ -0,0 +1,23 @@ +import 'package:dio/dio.dart'; + +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/core/services/dio_service.dart'; +import 'package:restaurant_tour/modules/home/repository/query.dart'; + +class HomeRepository { + Future> getRestaurants({int offset = 0}) async { + final result = await dio.post( + '/graphql', + data: query(offset), + options: Options( + headers: { + Headers.contentTypeHeader: 'application/graphql', + }, + ), + ); + final data = RestaurantQueryResult.fromJson( + result.data['data']['search'], + ); + return data.restaurants ?? []; + } +} diff --git a/lib/query.dart b/lib/modules/home/repository/query.dart similarity index 100% rename from lib/query.dart rename to lib/modules/home/repository/query.dart From a1e19dc852c74e6899b48639d28290edf746f9c3 Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Wed, 18 Sep 2024 03:17:54 -0600 Subject: [PATCH 10/15] load and add info to restaurant detail view --- lib/design_system/atoms/ds_image_network.dart | 70 +++++++++++------ lib/design_system/components/user_avatar.dart | 10 ++- lib/modules/home/home_page.dart | 7 ++ lib/modules/home/widgets/restaurant_card.dart | 1 + lib/modules/home/widgets/restaurant_list.dart | 7 +- .../restaurant_detail_page.dart | 77 ++++++++++++------- .../widgets/review_list.dart | 33 +++++--- 7 files changed, 139 insertions(+), 66 deletions(-) diff --git a/lib/design_system/atoms/ds_image_network.dart b/lib/design_system/atoms/ds_image_network.dart index c4afb7a..833685c 100644 --- a/lib/design_system/atoms/ds_image_network.dart +++ b/lib/design_system/atoms/ds_image_network.dart @@ -9,6 +9,7 @@ class DsImageNetwork extends StatelessWidget { this.fit, this.height, this.width = 100, + this.isRounded, }); final String? urlImage; @@ -17,6 +18,7 @@ class DsImageNetwork extends StatelessWidget { final BoxFit? fit; final double? height; final double width; + final bool? isRounded; @override Widget build(BuildContext context) { @@ -26,33 +28,53 @@ class DsImageNetwork extends StatelessWidget { color: Colors.grey.shade300, size: width, ) - : Padding( + : _RoundedBorderImage( + isRounded: isRounded ?? false, + child: Image.network( + urlImage!, + fit: fit ?? BoxFit.fill, + width: width, + height: height ?? width, + loadingBuilder: (context, child, loadingProgress) => + loadingProgress == null + ? child + : Center( + child: SizedBox( + width: width, + height: height ?? width, + child: const CircularProgressIndicator(), + ), + ), + errorBuilder: (context, error, stackTrace) => + errorIcon ?? + Icon( + Icons.image, + size: width, + ), + ), + ); + } +} + +class _RoundedBorderImage extends StatelessWidget { + const _RoundedBorderImage({ + required this.isRounded, + required this.child, + }); + + final bool isRounded; + final Widget child; + + @override + Widget build(BuildContext context) { + return isRounded + ? Padding( padding: const EdgeInsets.all(16.0), child: ClipRRect( borderRadius: BorderRadius.circular(8), - child: Image.network( - urlImage!, - fit: fit ?? BoxFit.fill, - width: width, - height: height ?? width, - loadingBuilder: (context, child, loadingProgress) => - loadingProgress == null - ? child - : Center( - child: SizedBox( - width: width, - height: height ?? width, - child: const CircularProgressIndicator(), - ), - ), - errorBuilder: (context, error, stackTrace) => - errorIcon ?? - Icon( - Icons.image, - size: width, - ), - ), + child: child, ), - ); + ) + : child; } } diff --git a/lib/design_system/components/user_avatar.dart b/lib/design_system/components/user_avatar.dart index 5664bc8..da610a1 100644 --- a/lib/design_system/components/user_avatar.dart +++ b/lib/design_system/components/user_avatar.dart @@ -17,8 +17,14 @@ class UserAvatar extends StatelessWidget { Widget build(BuildContext context) { return Row( children: [ - const CircleAvatar( - child: Icon(Icons.person), + CircleAvatar( + child: urlImage != null + ? ClipOval( + child: DsImageNetwork( + urlImage: urlImage, + ), + ) + : const Icon(Icons.person), ), const SizedBox(width: DsSizes.xxs), DsText(name), diff --git a/lib/modules/home/home_page.dart b/lib/modules/home/home_page.dart index b86cb5f..958d748 100644 --- a/lib/modules/home/home_page.dart +++ b/lib/modules/home/home_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/core/routes.dart'; import 'package:restaurant_tour/design_system/design_system.dart'; import 'package:restaurant_tour/modules/home/bloc/home_bloc.dart'; import 'package:restaurant_tour/modules/home/repository/home_repository.dart'; @@ -61,6 +62,12 @@ class _Content extends StatelessWidget { RestaurantList( loading: state is LoadingRestaurantsState, restaurants: state.model.restaurants ?? [], + onSelected: (restaurant) { + Navigator.of(context).pushNamed( + RoutePaths.restaurantDetail, + arguments: restaurant, + ); + }, ), const Center( child: Text('Tab 2'), diff --git a/lib/modules/home/widgets/restaurant_card.dart b/lib/modules/home/widgets/restaurant_card.dart index 3d7c923..37c8c12 100644 --- a/lib/modules/home/widgets/restaurant_card.dart +++ b/lib/modules/home/widgets/restaurant_card.dart @@ -24,6 +24,7 @@ class RestaurantCard extends StatelessWidget { children: [ DsImageNetwork( urlImage: restaurant.heroImage, + isRounded: true, ), Expanded( child: Column( diff --git a/lib/modules/home/widgets/restaurant_list.dart b/lib/modules/home/widgets/restaurant_list.dart index 70f73f3..a88a8ba 100644 --- a/lib/modules/home/widgets/restaurant_list.dart +++ b/lib/modules/home/widgets/restaurant_list.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; -import 'package:restaurant_tour/core/routes.dart'; import 'package:restaurant_tour/modules/home/widgets/restaurant_card.dart'; class RestaurantList extends StatelessWidget { @@ -9,10 +8,12 @@ class RestaurantList extends StatelessWidget { super.key, required this.loading, required this.restaurants, + required this.onSelected, }); final bool loading; final List restaurants; + final Function(Restaurant restaurant) onSelected; @override Widget build(BuildContext context) { @@ -27,9 +28,7 @@ class RestaurantList extends StatelessWidget { itemBuilder: (context, index) { final currentRestaurant = restaurants[index]; return InkWell( - onTap: () => Navigator.of(context).pushNamed( - RoutePaths.restaurantDetail, - ), + onTap: () => onSelected(currentRestaurant), child: RestaurantCard( restaurant: currentRestaurant, ), diff --git a/lib/modules/restaurant_detail/restaurant_detail_page.dart b/lib/modules/restaurant_detail/restaurant_detail_page.dart index b031ba3..180cd6f 100644 --- a/lib/modules/restaurant_detail/restaurant_detail_page.dart +++ b/lib/modules/restaurant_detail/restaurant_detail_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; import 'package:restaurant_tour/design_system/design_system.dart'; import 'package:restaurant_tour/modules/restaurant_detail/widgets/review_list.dart'; @@ -8,10 +9,25 @@ class RestaurantDetailPage extends StatelessWidget { @override Widget build(BuildContext context) { + return const _Content(); + } +} + +class _Content extends StatelessWidget { + const _Content(); + + @override + Widget build(BuildContext context) { + final args = ModalRoute.of(context)?.settings.arguments; + Restaurant? restaurant; + if (args != null) { + restaurant = args as Restaurant; + } + return Scaffold( appBar: AppBar( - title: const DsText( - 'Nombre del restaurante', + title: DsText( + restaurant?.name ?? 'No name', textVariant: TextVariant.subTitle, isBold: true, ), @@ -32,43 +48,52 @@ class RestaurantDetailPage extends StatelessWidget { height: MediaQuery.sizeOf(context).height * 0.4, width: double.infinity, color: Colors.grey, - child: const DsImageNetwork(), + child: DsImageNetwork( + urlImage: restaurant?.heroImage, + ), ), - const Padding( - padding: EdgeInsets.all(DsSizes.md), + Padding( + padding: const EdgeInsets.all(DsSizes.md), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 16), + const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - DsText('\$500 Italian'), - StatusOpen(isOpenNow: true), + DsText(restaurant?.price ?? '---'), + StatusOpen(isOpenNow: restaurant?.isOpen ?? false), ], ), - SizedBox(height: DsSizes.md), - Divider(), - SizedBox(height: DsSizes.md), - DsText('Address'), + const SizedBox(height: DsSizes.md), + const Divider(), + const SizedBox(height: DsSizes.md), + const DsText('Address'), DsText( - '102 Lakeside Ave', + restaurant?.location?.formattedAddress ?? 'No address', isBold: true, ), - DsText('Seattle, WA 98122', isBold: true), - SizedBox(height: DsSizes.md), - Divider(), - SizedBox(height: DsSizes.md), - DsText('Overral Rating'), - DsText( - '4.6', - textVariant: TextVariant.title, - isBold: true, + const SizedBox(height: DsSizes.md), + const Divider(), + const SizedBox(height: DsSizes.md), + const DsText('Overral Rating'), + Row( + children: [ + DsText( + (restaurant?.rating ?? 0).toString(), + textVariant: TextVariant.title, + isBold: true, + ), + const Icon( + Icons.star, + color: AppColors.stars, + ), + ], ), - SizedBox(height: DsSizes.md), - Divider(), - DsText('42 reviews'), - ReviewList(), + const SizedBox(height: DsSizes.md), + const Divider(), + DsText('${restaurant?.reviews?.length ?? 0} reviews'), + ReviewList(reviews: restaurant?.reviews ?? []), ], ), ), diff --git a/lib/modules/restaurant_detail/widgets/review_list.dart b/lib/modules/restaurant_detail/widgets/review_list.dart index 514a4a2..0cd6c6d 100644 --- a/lib/modules/restaurant_detail/widgets/review_list.dart +++ b/lib/modules/restaurant_detail/widgets/review_list.dart @@ -1,9 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; import 'package:restaurant_tour/design_system/design_system.dart'; class ReviewList extends StatelessWidget { - const ReviewList({super.key}); + const ReviewList({ + super.key, + required this.reviews, + }); + + final List reviews; @override Widget build(BuildContext context) { @@ -14,25 +20,32 @@ class ReviewList extends StatelessWidget { padding: const EdgeInsets.symmetric( vertical: DsSizes.md, ), - itemCount: 10, + itemCount: reviews.length, itemBuilder: (context, index) { - return const Padding( - padding: EdgeInsets.symmetric( + final currentReview = reviews[index]; + print(currentReview.text); + + return Padding( + padding: const EdgeInsets.symmetric( vertical: DsSizes.xxxs, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ DsRating( - initialRating: 3, + initialRating: currentReview.rating?.toDouble() ?? 0.0, itemCount: 5, ), - SizedBox(height: DsSizes.xxxs), - DsText( - 'Const class cannot remove fields: Library:package:restaurant_tour/modules/home/widgets/image_network_loading.dart Class: ImageNetworkLoading. Try performing a hot restart instead.', + const SizedBox(height: DsSizes.xxxs), + if (currentReview.text != null) + DsText( + currentReview.text ?? 'No content', + ), + const SizedBox(height: DsSizes.xxxs), + UserAvatar( + name: '${currentReview.user?.name}', + urlImage: currentReview.user?.imageUrl, ), - SizedBox(height: DsSizes.xxxs), - UserAvatar(name: 'User Name test'), ], ), ); From 22dbceec002dd6790790b7c1a10b391bdf266569 Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Wed, 18 Sep 2024 11:22:47 -0600 Subject: [PATCH 11/15] design: add skeleton list --- lib/design_system/components/components.dart | 1 + .../skeleton/ds_image_skeleton.dart | 27 +++ .../components/skeleton/ds_line_skeleton.dart | 30 +++ .../skeleton/ds_list_item_skeleton.dart | 45 ++++ .../components/skeleton/skeleton.dart | 3 + lib/modules/home/widgets/list_skeleton.dart | 15 ++ lib/modules/home/widgets/restaurant_list.dart | 5 +- pubspec.lock | 208 +++++++++++++++++- pubspec.yaml | 6 + 9 files changed, 333 insertions(+), 7 deletions(-) create mode 100644 lib/design_system/components/skeleton/ds_image_skeleton.dart create mode 100644 lib/design_system/components/skeleton/ds_line_skeleton.dart create mode 100644 lib/design_system/components/skeleton/ds_list_item_skeleton.dart create mode 100644 lib/design_system/components/skeleton/skeleton.dart create mode 100644 lib/modules/home/widgets/list_skeleton.dart diff --git a/lib/design_system/components/components.dart b/lib/design_system/components/components.dart index a97262f..eca17f5 100644 --- a/lib/design_system/components/components.dart +++ b/lib/design_system/components/components.dart @@ -1,2 +1,3 @@ export 'status_open.dart'; export 'user_avatar.dart'; +export 'skeleton/skeleton.dart'; diff --git a/lib/design_system/components/skeleton/ds_image_skeleton.dart b/lib/design_system/components/skeleton/ds_image_skeleton.dart new file mode 100644 index 0000000..8cc94fe --- /dev/null +++ b/lib/design_system/components/skeleton/ds_image_skeleton.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +import 'package:shimmer/shimmer.dart'; + +class DsImageSkeleton extends StatelessWidget { + const DsImageSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular( + 16, + ), + ), + color: Colors.white, + ), + ), + ); + } +} diff --git a/lib/design_system/components/skeleton/ds_line_skeleton.dart b/lib/design_system/components/skeleton/ds_line_skeleton.dart new file mode 100644 index 0000000..8d057a9 --- /dev/null +++ b/lib/design_system/components/skeleton/ds_line_skeleton.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +import 'package:shimmer/shimmer.dart'; + +class DsLineSkeleton extends StatelessWidget { + const DsLineSkeleton({ + super.key, + required this.width, + }); + + final double width; + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey.shade300, + highlightColor: Colors.grey.shade100, + child: Container( + width: width, + height: 15, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 10, + ), + color: Colors.white, + ), + ), + ); + } +} diff --git a/lib/design_system/components/skeleton/ds_list_item_skeleton.dart b/lib/design_system/components/skeleton/ds_list_item_skeleton.dart new file mode 100644 index 0000000..58a844e --- /dev/null +++ b/lib/design_system/components/skeleton/ds_list_item_skeleton.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +import 'package:restaurant_tour/design_system/components/skeleton/skeleton.dart'; +import 'package:restaurant_tour/design_system/tokens/tokens.dart'; + +class DsListItemSkeleton extends StatelessWidget { + const DsListItemSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.all(8), + child: Row( + children: [ + const DsImageSkeleton(), + const SizedBox(width: DsSizes.md), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DsLineSkeleton( + width: MediaQuery.sizeOf(context).width * 0.9, + ), + const SizedBox(height: DsSizes.md), + const Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + DsLineSkeleton(width: 100), + DsLineSkeleton(width: 100), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/design_system/components/skeleton/skeleton.dart b/lib/design_system/components/skeleton/skeleton.dart new file mode 100644 index 0000000..b7fef39 --- /dev/null +++ b/lib/design_system/components/skeleton/skeleton.dart @@ -0,0 +1,3 @@ +export 'ds_image_skeleton.dart'; +export 'ds_line_skeleton.dart'; +export 'ds_list_item_skeleton.dart'; diff --git a/lib/modules/home/widgets/list_skeleton.dart b/lib/modules/home/widgets/list_skeleton.dart new file mode 100644 index 0000000..9f33f7e --- /dev/null +++ b/lib/modules/home/widgets/list_skeleton.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +import 'package:restaurant_tour/design_system/components/components.dart'; + +class ListSkeleton extends StatelessWidget { + const ListSkeleton({super.key}); + + @override + Widget build(BuildContext context) { + return ListView.builder( + itemCount: 5, + itemBuilder: (context, index) => const DsListItemSkeleton(), + ); + } +} diff --git a/lib/modules/home/widgets/restaurant_list.dart b/lib/modules/home/widgets/restaurant_list.dart index a88a8ba..e6670ed 100644 --- a/lib/modules/home/widgets/restaurant_list.dart +++ b/lib/modules/home/widgets/restaurant_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/modules/home/widgets/list_skeleton.dart'; import 'package:restaurant_tour/modules/home/widgets/restaurant_card.dart'; class RestaurantList extends StatelessWidget { @@ -18,9 +19,7 @@ class RestaurantList extends StatelessWidget { @override Widget build(BuildContext context) { if (loading) { - return const Center( - child: CircularProgressIndicator(), - ); + return const ListSkeleton(); } return ListView.builder( diff --git a/pubspec.lock b/pubspec.lock index a18fb0b..2a9314a 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 dev" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -169,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" + url: "https://pub.dev" + source: hosted + version: "1.8.0" crypto: dependency: transitive description: @@ -185,6 +201,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + 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: @@ -217,6 +241,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: @@ -335,10 +367,10 @@ packages: 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: @@ -427,6 +459,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" nested: dependency: transitive description: @@ -435,6 +475,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" package_config: dependency: transitive description: @@ -444,13 +492,77 @@ packages: source: hosted version: "2.0.2" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 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: @@ -483,6 +595,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + sembast: + dependency: "direct main" + description: + name: sembast + sha256: a49ce14fb0d81bee9f8941061a38f4b790d19c0ab01abe35a529c1fcef0512a1 + url: "https://pub.dev" + source: hosted + version: "3.7.2" shelf: dependency: transitive description: @@ -491,6 +611,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + 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: @@ -499,6 +635,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 @@ -520,6 +664,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: @@ -560,6 +720,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: @@ -568,6 +736,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: @@ -576,6 +752,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: @@ -632,6 +816,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.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: @@ -642,4 +842,4 @@ packages: version: "3.1.0" 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 50077f0..b0e98c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,10 @@ dependencies: flutter_rating_bar: ^4.0.1 http: ^1.2.2 json_annotation: ^4.9.0 + path: ^1.9.0 + path_provider: ^2.1.4 + sembast: ^3.7.2 + shimmer: ^3.0.0 dev_dependencies: flutter_test: @@ -27,6 +31,8 @@ dev_dependencies: flutter_lints: ^4.0.0 build_runner: ^2.4.10 json_serializable: ^6.8.0 + bloc_test: ^9.1.7 + mocktail: ^1.0.4 flutter: generate: true From a714842450ffcd2253f61c2a4d74d3dbb05f1882 Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Wed, 18 Sep 2024 11:24:41 -0600 Subject: [PATCH 12/15] feat: add database to save favorites --- lib/core/database/app_database.dart | 37 +++++++++++++++++++ lib/core/database/database.dart | 1 + .../database/favorite_restaurants_dao.dart | 36 ++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 lib/core/database/app_database.dart create mode 100644 lib/core/database/database.dart create mode 100644 lib/core/database/favorite_restaurants_dao.dart diff --git a/lib/core/database/app_database.dart b/lib/core/database/app_database.dart new file mode 100644 index 0000000..9f6fdcb --- /dev/null +++ b/lib/core/database/app_database.dart @@ -0,0 +1,37 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; + +class AppDatabase { + static final AppDatabase _instance = AppDatabase._internal(); + static final DatabaseFactory _dbFactory = databaseFactoryIo; + static Database? _database; + + factory AppDatabase() => _instance; + + AppDatabase._internal(); + + Future get database async { + if (_database != null) return _database!; + + _database = await _initDatabase(); + return _database!; + } + + Future _initDatabase() async { + Directory appDocDir = await getApplicationDocumentsDirectory(); + String dbPath = path.join(appDocDir.path, 'flutter_test.db'); + Database database = await _dbFactory.openDatabase(dbPath); + return database; + } + + Future closeDatabase() async { + if (_database != null) { + await _database!.close(); + _database = null; + } + } +} diff --git a/lib/core/database/database.dart b/lib/core/database/database.dart new file mode 100644 index 0000000..0bcdf68 --- /dev/null +++ b/lib/core/database/database.dart @@ -0,0 +1 @@ +export 'favorite_restaurants_dao.dart'; diff --git a/lib/core/database/favorite_restaurants_dao.dart b/lib/core/database/favorite_restaurants_dao.dart new file mode 100644 index 0000000..f948ad4 --- /dev/null +++ b/lib/core/database/favorite_restaurants_dao.dart @@ -0,0 +1,36 @@ +import 'dart:async'; + +import 'package:sembast/sembast.dart'; + +import 'package:restaurant_tour/core/database/app_database.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; + +class FavoriteRestaurantsDao { + static const String favoriteRestaurantsStoreName = 'favorite_restaurants'; + + final _favoriteRestaurantsStore = + intMapStoreFactory.store(favoriteRestaurantsStoreName); + + Future get _db async => AppDatabase().database; + + Future insert(Restaurant restaurant) async { + await _favoriteRestaurantsStore.add(await _db, restaurant.toJson()); + } + + Future> getAll() async { + final list = await _favoriteRestaurantsStore.find(await _db); + return list.map((e) => Restaurant.fromJson(e.value)).toList(); + } + + Future delete({required String restaurantId}) async { + await _favoriteRestaurantsStore.delete( + await _db, + finder: Finder( + filter: Filter.equals( + 'id', + restaurantId, + ), + ), + ); + } +} From 35b4c14c64d0a4744550a4916093a6c0f3dc0ef3 Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Wed, 18 Sep 2024 11:25:43 -0600 Subject: [PATCH 13/15] feat: add list to favorites tab --- lib/modules/home/bloc/home_bloc.dart | 17 ++++++- lib/modules/home/bloc/home_event.dart | 7 ++- lib/modules/home/bloc/home_state.dart | 27 +++++++++-- lib/modules/home/home_page.dart | 48 +++++++++++++++---- .../home/repository/home_repository.dart | 7 +++ 5 files changed, 90 insertions(+), 16 deletions(-) diff --git a/lib/modules/home/bloc/home_bloc.dart b/lib/modules/home/bloc/home_bloc.dart index 75908db..3806d52 100644 --- a/lib/modules/home/bloc/home_bloc.dart +++ b/lib/modules/home/bloc/home_bloc.dart @@ -12,7 +12,11 @@ part 'home_state.dart'; class HomeBloc extends Bloc { HomeBloc({required this.homeRepository}) : super( - const HomeInitialState(Model()), + const HomeInitialState( + Model( + initialIndex: 0, + ), + ), ) { on(_onLoadRestaurantsEvent); } @@ -25,11 +29,20 @@ class HomeBloc extends Bloc { ) async { emit(LoadingRestaurantsState(state.model)); try { - final restaurants = await homeRepository.getRestaurants(); + final onlyFavorites = event.onlyFavorites; + List restaurants = state.model.restaurants ?? []; + if (!onlyFavorites) { + restaurants = await homeRepository.getRestaurants(); + } + + final favoriteRestaurants = await homeRepository.getFavoriteRestaurants(); + emit( LoadedRestaurantsState( state.model.copyWith( restaurants: restaurants, + favoriteRestaurants: favoriteRestaurants, + initialIndex: onlyFavorites ? 1 : 0, ), ), ); diff --git a/lib/modules/home/bloc/home_event.dart b/lib/modules/home/bloc/home_event.dart index 22d33ce..91056dc 100644 --- a/lib/modules/home/bloc/home_event.dart +++ b/lib/modules/home/bloc/home_event.dart @@ -8,5 +8,10 @@ sealed class HomeEvent extends Equatable { } class LoadRestaurantsEvent extends HomeEvent { - const LoadRestaurantsEvent(); + final bool onlyFavorites; + + const LoadRestaurantsEvent({this.onlyFavorites = false}); + + @override + List get props => [onlyFavorites]; } diff --git a/lib/modules/home/bloc/home_state.dart b/lib/modules/home/bloc/home_state.dart index 3fa367d..1cc4704 100644 --- a/lib/modules/home/bloc/home_state.dart +++ b/lib/modules/home/bloc/home_state.dart @@ -27,13 +27,30 @@ final class ErrorLoadRestaurantsState extends HomeState { class Model extends Equatable { final List? restaurants; - - const Model({this.restaurants}); - - Model copyWith({List? restaurants}) => Model( + final List? favoriteRestaurants; + final int? initialIndex; + + const Model({ + this.restaurants, + this.favoriteRestaurants, + this.initialIndex, + }); + + Model copyWith({ + List? restaurants, + List? favoriteRestaurants, + int? initialIndex, + }) => + Model( restaurants: restaurants ?? this.restaurants, + favoriteRestaurants: favoriteRestaurants ?? this.favoriteRestaurants, + initialIndex: initialIndex ?? this.initialIndex, ); @override - List get props => [restaurants]; + List get props => [ + restaurants, + favoriteRestaurants, + initialIndex, + ]; } diff --git a/lib/modules/home/home_page.dart b/lib/modules/home/home_page.dart index 958d748..58aca83 100644 --- a/lib/modules/home/home_page.dart +++ b/lib/modules/home/home_page.dart @@ -2,11 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; import 'package:restaurant_tour/core/routes.dart'; import 'package:restaurant_tour/design_system/design_system.dart'; import 'package:restaurant_tour/modules/home/bloc/home_bloc.dart'; import 'package:restaurant_tour/modules/home/repository/home_repository.dart'; import 'package:restaurant_tour/modules/home/widgets/restaurant_list.dart'; +import 'package:restaurant_tour/modules/restaurant_detail/models/detail_page_arguments.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @@ -41,6 +43,7 @@ class _Content extends StatelessWidget { builder: (context, state) { return DefaultTabController( length: 2, + initialIndex: state.model.initialIndex ?? 0, child: Scaffold( appBar: AppBar( title: const DsText( @@ -62,15 +65,20 @@ class _Content extends StatelessWidget { RestaurantList( loading: state is LoadingRestaurantsState, restaurants: state.model.restaurants ?? [], - onSelected: (restaurant) { - Navigator.of(context).pushNamed( - RoutePaths.restaurantDetail, - arguments: restaurant, - ); - }, + onSelected: (restaurant) => _goDetail( + context, + restaurant: restaurant, + isFromFavorite: false, + ), ), - const Center( - child: Text('Tab 2'), + RestaurantList( + loading: state is LoadingRestaurantsState, + restaurants: state.model.favoriteRestaurants ?? [], + onSelected: (restaurant) => _goDetail( + context, + restaurant: restaurant, + isFromFavorite: true, + ), ), ], ), @@ -79,4 +87,28 @@ class _Content extends StatelessWidget { }, ); } + + _goDetail( + BuildContext context, { + required Restaurant restaurant, + required bool isFromFavorite, + }) { + Navigator.of(context) + .pushNamed( + RoutePaths.restaurantDetail, + arguments: DetailPageArgurments( + restaurant: restaurant, + isFromFavorite: isFromFavorite, + ), + ) + .then((value) { + if (value == true) { + context.read().add( + LoadRestaurantsEvent( + onlyFavorites: isFromFavorite, + ), + ); + } + }); + } } diff --git a/lib/modules/home/repository/home_repository.dart b/lib/modules/home/repository/home_repository.dart index b5b4a15..e8dde70 100644 --- a/lib/modules/home/repository/home_repository.dart +++ b/lib/modules/home/repository/home_repository.dart @@ -1,10 +1,13 @@ import 'package:dio/dio.dart'; +import 'package:restaurant_tour/core/database/database.dart'; import 'package:restaurant_tour/core/models/restaurant.dart'; import 'package:restaurant_tour/core/services/dio_service.dart'; import 'package:restaurant_tour/modules/home/repository/query.dart'; class HomeRepository { + final _favoriteRestaurantsDao = FavoriteRestaurantsDao(); + Future> getRestaurants({int offset = 0}) async { final result = await dio.post( '/graphql', @@ -20,4 +23,8 @@ class HomeRepository { ); return data.restaurants ?? []; } + + Future> getFavoriteRestaurants() async { + return await _favoriteRestaurantsDao.getAll(); + } } From 72f4b471c12f9fe0e7b06c2bc5dcf5b247f543ff Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Wed, 18 Sep 2024 11:26:26 -0600 Subject: [PATCH 14/15] feat: add function to set favorite restaurant --- .../bloc/restaurant_detail_bloc.dart | 81 ++++++++ .../bloc/restaurant_detail_event.dart | 26 +++ .../bloc/restaurant_detail_state.dart | 56 +++++ .../models/detail_page_arguments.dart | 11 + .../restaurant_detail_page.dart | 192 +++++++++++------- .../restaurant_detail_repository.dart | 14 ++ 6 files changed, 305 insertions(+), 75 deletions(-) create mode 100644 lib/modules/restaurant_detail/bloc/restaurant_detail_bloc.dart create mode 100644 lib/modules/restaurant_detail/bloc/restaurant_detail_event.dart create mode 100644 lib/modules/restaurant_detail/bloc/restaurant_detail_state.dart create mode 100644 lib/modules/restaurant_detail/models/detail_page_arguments.dart create mode 100644 lib/modules/restaurant_detail/restaurant_detail_repository.dart diff --git a/lib/modules/restaurant_detail/bloc/restaurant_detail_bloc.dart b/lib/modules/restaurant_detail/bloc/restaurant_detail_bloc.dart new file mode 100644 index 0000000..ece362f --- /dev/null +++ b/lib/modules/restaurant_detail/bloc/restaurant_detail_bloc.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/modules/restaurant_detail/models/detail_page_arguments.dart'; +import 'package:restaurant_tour/modules/restaurant_detail/restaurant_detail_repository.dart'; + +part 'restaurant_detail_event.dart'; +part 'restaurant_detail_state.dart'; + +class RestaurantDetailBloc + extends Bloc { + RestaurantDetailBloc({ + required this.restaurantDetailRepository, + }) : super( + const RestaurantDetailInitialState(Model()), + ) { + on(_onLoadDetailEvent); + on(_onSetFavoriteEvent); + } + + final RestaurantDetailRepository restaurantDetailRepository; + + _onLoadDetailEvent( + LoadDetailEvent event, + Emitter emit, + ) { + emit(LoadingDetailState(state.model)); + final restaurant = event.detailPageArgurments?.restaurant; + final isFromFavorite = event.detailPageArgurments?.isFromFavorite ?? false; + + if (restaurant != null) { + emit( + LoadedDetailState( + state.model.copyWith( + restaurant: restaurant, + isFavorite: isFromFavorite, + ), + ), + ); + return; + } + + emit(ErrorLoadDetailState(state.model)); + } + + _onSetFavoriteEvent( + SetFavoriteEvent event, + Emitter emit, + ) async { + emit(LoadingSetFavoriteState(state.model)); + final isFavorite = event.isFavorite; + final restaurant = state.model.restaurant!; + + try { + if (isFavorite) { + await restaurantDetailRepository.addRestaurant( + restaurant: restaurant, + ); + } else { + await restaurantDetailRepository.deleteRestaurant( + restaurantId: restaurant.id ?? '', + ); + } + + emit( + LoadedSetFavoriteState( + state.model.copyWith( + isFavorite: isFavorite, + ), + ), + ); + } catch (e, stacktrace) { + debugPrint(e.toString()); + debugPrint(stacktrace.toString()); + emit(ErrorSetFavoriteState(state.model)); + } + } +} diff --git a/lib/modules/restaurant_detail/bloc/restaurant_detail_event.dart b/lib/modules/restaurant_detail/bloc/restaurant_detail_event.dart new file mode 100644 index 0000000..b1bb202 --- /dev/null +++ b/lib/modules/restaurant_detail/bloc/restaurant_detail_event.dart @@ -0,0 +1,26 @@ +part of 'restaurant_detail_bloc.dart'; + +sealed class RestaurantDetailEvent extends Equatable { + const RestaurantDetailEvent(); + + @override + List get props => []; +} + +class LoadDetailEvent extends RestaurantDetailEvent { + final DetailPageArgurments? detailPageArgurments; + + const LoadDetailEvent({this.detailPageArgurments}); + + @override + List get props => [detailPageArgurments]; +} + +class SetFavoriteEvent extends RestaurantDetailEvent { + final bool isFavorite; + + const SetFavoriteEvent({required this.isFavorite}); + + @override + List get props => [isFavorite]; +} diff --git a/lib/modules/restaurant_detail/bloc/restaurant_detail_state.dart b/lib/modules/restaurant_detail/bloc/restaurant_detail_state.dart new file mode 100644 index 0000000..ebb488c --- /dev/null +++ b/lib/modules/restaurant_detail/bloc/restaurant_detail_state.dart @@ -0,0 +1,56 @@ +part of 'restaurant_detail_bloc.dart'; + +sealed class RestaurantDetailState extends Equatable { + final Model model; + + const RestaurantDetailState(this.model); + + @override + List get props => [model]; +} + +final class RestaurantDetailInitialState extends RestaurantDetailState { + const RestaurantDetailInitialState(super.model); +} + +final class LoadingDetailState extends RestaurantDetailState { + const LoadingDetailState(super.model); +} + +final class LoadedDetailState extends RestaurantDetailState { + const LoadedDetailState(super.model); +} + +final class ErrorLoadDetailState extends RestaurantDetailState { + const ErrorLoadDetailState(super.model); +} + +final class LoadingSetFavoriteState extends RestaurantDetailState { + const LoadingSetFavoriteState(super.model); +} + +final class LoadedSetFavoriteState extends RestaurantDetailState { + const LoadedSetFavoriteState(super.model); +} + +final class ErrorSetFavoriteState extends RestaurantDetailState { + const ErrorSetFavoriteState(super.model); +} + +class Model extends Equatable { + final Restaurant? restaurant; + final bool? isFavorite; + + const Model({this.isFavorite, this.restaurant}); + + Model copyWith({bool? isFavorite, Restaurant? restaurant}) => Model( + isFavorite: isFavorite ?? this.isFavorite, + restaurant: restaurant ?? this.restaurant, + ); + + @override + List get props => [ + isFavorite, + restaurant, + ]; +} diff --git a/lib/modules/restaurant_detail/models/detail_page_arguments.dart b/lib/modules/restaurant_detail/models/detail_page_arguments.dart new file mode 100644 index 0000000..66375f5 --- /dev/null +++ b/lib/modules/restaurant_detail/models/detail_page_arguments.dart @@ -0,0 +1,11 @@ +import 'package:restaurant_tour/core/models/restaurant.dart'; + +class DetailPageArgurments { + final Restaurant restaurant; + final bool isFromFavorite; + + DetailPageArgurments({ + required this.restaurant, + required this.isFromFavorite, + }); +} diff --git a/lib/modules/restaurant_detail/restaurant_detail_page.dart b/lib/modules/restaurant_detail/restaurant_detail_page.dart index 180cd6f..07d6a6d 100644 --- a/lib/modules/restaurant_detail/restaurant_detail_page.dart +++ b/lib/modules/restaurant_detail/restaurant_detail_page.dart @@ -1,7 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + import 'package:restaurant_tour/design_system/design_system.dart'; +import 'package:restaurant_tour/modules/restaurant_detail/bloc/restaurant_detail_bloc.dart'; +import 'package:restaurant_tour/modules/restaurant_detail/models/detail_page_arguments.dart'; +import 'package:restaurant_tour/modules/restaurant_detail/restaurant_detail_repository.dart'; import 'package:restaurant_tour/modules/restaurant_detail/widgets/review_list.dart'; class RestaurantDetailPage extends StatelessWidget { @@ -19,86 +23,124 @@ class _Content extends StatelessWidget { @override Widget build(BuildContext context) { final args = ModalRoute.of(context)?.settings.arguments; - Restaurant? restaurant; - if (args != null) { - restaurant = args as Restaurant; - } + final detailPageArgurments = + args != null ? args as DetailPageArgurments : null; - return Scaffold( - appBar: AppBar( - title: DsText( - restaurant?.name ?? 'No name', - textVariant: TextVariant.subTitle, - isBold: true, - ), - centerTitle: true, - actions: [ - IconButton( - onPressed: () {}, - icon: const Icon( - Icons.favorite, - ), + return BlocProvider( + create: (context) => RestaurantDetailBloc( + restaurantDetailRepository: RestaurantDetailRepository(), + )..add( + LoadDetailEvent( + detailPageArgurments: detailPageArgurments, ), - ], - ), - body: SingleChildScrollView( - child: Column( - children: [ - Container( - height: MediaQuery.sizeOf(context).height * 0.4, - width: double.infinity, - color: Colors.grey, - child: DsImageNetwork( - urlImage: restaurant?.heroImage, + ), + child: BlocConsumer( + listener: (context, state) { + if (state is ErrorLoadDetailState) { + context.showDsSnackBar( + message: 'Error when loaded details', + type: SnackBarType.error, + ); + } + }, + builder: (context, state) { + final restaurant = state.model.restaurant; + final isFavorite = state.model.isFavorite ?? false; + + return Scaffold( + appBar: AppBar( + title: DsText( + restaurant?.name ?? 'No name', + textVariant: TextVariant.subTitle, + isBold: true, ), - ), - Padding( - padding: const EdgeInsets.all(DsSizes.md), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - DsText(restaurant?.price ?? '---'), - StatusOpen(isOpenNow: restaurant?.isOpen ?? false), - ], - ), - const SizedBox(height: DsSizes.md), - const Divider(), - const SizedBox(height: DsSizes.md), - const DsText('Address'), - DsText( - restaurant?.location?.formattedAddress ?? 'No address', - isBold: true, - ), - const SizedBox(height: DsSizes.md), - const Divider(), - const SizedBox(height: DsSizes.md), - const DsText('Overral Rating'), - Row( - children: [ - DsText( - (restaurant?.rating ?? 0).toString(), - textVariant: TextVariant.title, - isBold: true, - ), - const Icon( - Icons.star, - color: AppColors.stars, + centerTitle: true, + actions: [ + IconButton( + onPressed: () => context.read().add( + SetFavoriteEvent( + isFavorite: !isFavorite, + ), ), - ], + icon: Icon( + isFavorite + ? Icons.favorite + : Icons.favorite_outline_rounded, ), - const SizedBox(height: DsSizes.md), - const Divider(), - DsText('${restaurant?.reviews?.length ?? 0} reviews'), - ReviewList(reviews: restaurant?.reviews ?? []), - ], - ), + ), + ], ), - ], - ), + body: state is LoadingDetailState + ? const Center( + child: CircularProgressIndicator(), + ) + : SingleChildScrollView( + child: Column( + children: [ + Container( + height: MediaQuery.sizeOf(context).height * 0.4, + width: double.infinity, + color: Colors.grey, + child: DsImageNetwork( + urlImage: restaurant?.heroImage, + ), + ), + Padding( + padding: const EdgeInsets.all(DsSizes.md), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + DsText(restaurant?.price ?? '---'), + StatusOpen( + isOpenNow: restaurant?.isOpen ?? false, + ), + ], + ), + const SizedBox(height: DsSizes.md), + const Divider(), + const SizedBox(height: DsSizes.md), + const DsText('Address'), + DsText( + restaurant?.location?.formattedAddress ?? + 'No address', + isBold: true, + ), + const SizedBox(height: DsSizes.md), + const Divider(), + const SizedBox(height: DsSizes.md), + const DsText('Overral Rating'), + Row( + children: [ + DsText( + (restaurant?.rating ?? 0).toString(), + textVariant: TextVariant.title, + isBold: true, + ), + const Icon( + Icons.star, + color: AppColors.stars, + ), + ], + ), + const SizedBox(height: DsSizes.md), + const Divider(), + DsText( + '${restaurant?.reviews?.length ?? 0} reviews', + ), + ReviewList(reviews: restaurant?.reviews ?? []), + ], + ), + ), + ], + ), + ), + ); + }, ), ); } diff --git a/lib/modules/restaurant_detail/restaurant_detail_repository.dart b/lib/modules/restaurant_detail/restaurant_detail_repository.dart new file mode 100644 index 0000000..c5f5849 --- /dev/null +++ b/lib/modules/restaurant_detail/restaurant_detail_repository.dart @@ -0,0 +1,14 @@ +import 'package:restaurant_tour/core/database/database.dart'; +import 'package:restaurant_tour/core/models/restaurant.dart'; + +class RestaurantDetailRepository { + final _favoriteRestaurantsDao = FavoriteRestaurantsDao(); + + Future addRestaurant({required Restaurant restaurant}) async { + await _favoriteRestaurantsDao.insert(restaurant); + } + + Future deleteRestaurant({required String restaurantId}) async { + await _favoriteRestaurantsDao.delete(restaurantId: restaurantId); + } +} From 61a5e6a40710106d1de792609bc4bf4030cd3885 Mon Sep 17 00:00:00 2001 From: Ariel Lugo Date: Wed, 18 Sep 2024 11:27:03 -0600 Subject: [PATCH 15/15] test: add test to home_bloc and restaurant_detail_bloc --- ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Podfile | 44 +++++ lib/typography.dart | 49 ------ test/modules/home/bloc/home_bloc_test.dart | 92 +++++++++++ .../bloc/restaurant_detail_bloc_test.dart | 151 ++++++++++++++++++ test/widget_test.dart | 19 --- 7 files changed, 289 insertions(+), 68 deletions(-) create mode 100644 ios/Podfile delete mode 100644 lib/typography.dart create mode 100644 test/modules/home/bloc/home_bloc_test.dart create mode 100644 test/modules/restaurant_detail/bloc/restaurant_detail_bloc_test.dart delete mode 100644 test/widget_test.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/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/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/typography.dart b/lib/typography.dart deleted file mode 100644 index e165260..0000000 --- a/lib/typography.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppTextStyles { - ////----- Lora -----// - static const loraRegularHeadline = TextStyle( - fontFamily: 'Lora', - fontWeight: FontWeight.w700, - fontSize: 18.0, - ); - static const loraRegularTitle = TextStyle( - fontFamily: 'Lora', - fontWeight: FontWeight.w500, - fontSize: 16.0, - ); - - //----- Open Sans -----// - static const openRegularHeadline = TextStyle( - fontFamily: 'OpenSans', - fontWeight: FontWeight.w400, - fontSize: 16.0, - color: Colors.black, - ); - static const openRegularTitleSemiBold = TextStyle( - fontFamily: 'OpenSans', - fontWeight: FontWeight.w600, - fontSize: 14.0, - color: Colors.black, - ); - static const openRegularTitle = TextStyle( - fontFamily: 'OpenSans', - fontWeight: FontWeight.w400, - fontSize: 14.0, - color: Colors.black, - ); - static const openRegularText = TextStyle( - fontFamily: 'OpenSans', - fontWeight: FontWeight.w400, - fontSize: 12.0, - color: Colors.black, - ); - - static const openRegularItalic = TextStyle( - fontFamily: 'OpenSans', - fontWeight: FontWeight.w400, - fontStyle: FontStyle.italic, - fontSize: 12.0, - color: Colors.black, - ); -} diff --git a/test/modules/home/bloc/home_bloc_test.dart b/test/modules/home/bloc/home_bloc_test.dart new file mode 100644 index 0000000..f079fdf --- /dev/null +++ b/test/modules/home/bloc/home_bloc_test.dart @@ -0,0 +1,92 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/modules/home/bloc/home_bloc.dart'; +import 'package:restaurant_tour/modules/home/repository/home_repository.dart'; + +class MockHomeRepository extends Mock implements HomeRepository { + @override + Future> getRestaurants({int offset = 0}) { + return Future.value([]); + } + + @override + Future> getFavoriteRestaurants() { + return Future.value([]); + } +} + +class MockHomeRepositoryWithError extends Mock implements HomeRepository { + @override + Future> getRestaurants({int offset = 0}) { + return Future.error(Error()); + } + + @override + Future> getFavoriteRestaurants() { + return Future.error(Error()); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group( + 'homeBlocTest', + () { + late MockHomeRepository mockHomeRepository; + late MockHomeRepositoryWithError mockHomeRepositoryWithError; + + setUp( + () => { + mockHomeRepository = MockHomeRepository(), + mockHomeRepositoryWithError = MockHomeRepositoryWithError(), + }, + ); + + blocTest( + 'should emit LoadedRestaurantsState when LoadRestaurantsEvent is called and onlyFavorites is false', + build: () => HomeBloc( + homeRepository: mockHomeRepository, + ), + act: (bloc) => bloc.add( + const LoadRestaurantsEvent(), + ), + expect: () => [ + isA(), + isA(), + ], + ); + + blocTest( + 'should emit LoadedRestaurantsState when LoadRestaurantsEvent is called and onlyFavorites is true', + build: () => HomeBloc( + homeRepository: mockHomeRepository, + ), + act: (bloc) => bloc.add( + const LoadRestaurantsEvent(onlyFavorites: true), + ), + expect: () => [ + isA(), + isA(), + ], + ); + + blocTest( + 'should emit ErrorLoadRestaurantsState when LoadRestaurantsEvent is called', + build: () => HomeBloc( + homeRepository: mockHomeRepositoryWithError, + ), + act: (bloc) => bloc.add( + const LoadRestaurantsEvent(), + ), + expect: () => [ + isA(), + isA(), + ], + ); + }, + ); +} diff --git a/test/modules/restaurant_detail/bloc/restaurant_detail_bloc_test.dart b/test/modules/restaurant_detail/bloc/restaurant_detail_bloc_test.dart new file mode 100644 index 0000000..254adc3 --- /dev/null +++ b/test/modules/restaurant_detail/bloc/restaurant_detail_bloc_test.dart @@ -0,0 +1,151 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'package:restaurant_tour/core/models/restaurant.dart'; +import 'package:restaurant_tour/modules/restaurant_detail/bloc/restaurant_detail_bloc.dart'; +import 'package:restaurant_tour/modules/restaurant_detail/models/detail_page_arguments.dart'; +import 'package:restaurant_tour/modules/restaurant_detail/restaurant_detail_repository.dart'; + +class MockRestaurantDetailRepository extends Mock + implements RestaurantDetailRepository { + @override + Future addRestaurant({required Restaurant restaurant}) { + return Future.value(); + } + + @override + Future deleteRestaurant({required String restaurantId}) { + return Future.value(); + } +} + +class MockRestaurantDetailRepositoryWithError extends Mock + implements RestaurantDetailRepository { + @override + Future addRestaurant({required Restaurant restaurant}) { + return Future.error(Error()); + } + + @override + Future deleteRestaurant({required String restaurantId}) { + return Future.error(Error()); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group( + 'restaurantDetailBlocTest', + () { + late MockRestaurantDetailRepository mockRestaurantDetailRepository; + late MockRestaurantDetailRepositoryWithError + mockRestaurantDetailRepositoryWithError; + + setUp( + () => { + mockRestaurantDetailRepository = MockRestaurantDetailRepository(), + mockRestaurantDetailRepositoryWithError = + MockRestaurantDetailRepositoryWithError(), + }, + ); + + blocTest( + 'should emit LoadedDetailState when LoadDetailEvent is called', + build: () => RestaurantDetailBloc( + restaurantDetailRepository: mockRestaurantDetailRepository, + ), + act: (bloc) => bloc.add( + LoadDetailEvent( + detailPageArgurments: DetailPageArgurments( + restaurant: const Restaurant(), + isFromFavorite: true, + ), + ), + ), + expect: () => [ + isA(), + isA(), + ], + ); + + blocTest( + 'should emit ErrorLoadDetailState when LoadDetailEvent is called', + build: () => RestaurantDetailBloc( + restaurantDetailRepository: mockRestaurantDetailRepository, + ), + act: (bloc) => bloc.add( + const LoadDetailEvent(), + ), + expect: () => [ + isA(), + isA(), + ], + ); + + blocTest( + 'should emit LoadedSetFavoriteState when SetFavoriteEvent is called and isFavorite is true', + build: () => RestaurantDetailBloc( + restaurantDetailRepository: mockRestaurantDetailRepository, + ), + seed: () => const RestaurantDetailInitialState( + Model( + restaurant: Restaurant(), + ), + ), + act: (bloc) => bloc.add( + const SetFavoriteEvent( + isFavorite: true, + ), + ), + expect: () => [ + isA(), + isA(), + ], + ); + + blocTest( + 'should emit LoadedSetFavoriteState when SetFavoriteEvent is called and isFavorite is false', + build: () => RestaurantDetailBloc( + restaurantDetailRepository: mockRestaurantDetailRepository, + ), + seed: () => const RestaurantDetailInitialState( + Model( + restaurant: Restaurant(), + ), + ), + act: (bloc) => bloc.add( + const SetFavoriteEvent( + isFavorite: false, + ), + ), + expect: () => [ + isA(), + isA(), + ], + ); + + blocTest( + 'should emit ErrorSetFavoriteState when SetFavoriteEvent is called', + build: () => RestaurantDetailBloc( + restaurantDetailRepository: mockRestaurantDetailRepositoryWithError, + ), + seed: () => const RestaurantDetailInitialState( + Model( + restaurant: Restaurant(), + ), + ), + act: (bloc) => bloc.add( + const SetFavoriteEvent( + isFavorite: false, + ), + ), + expect: () => [ + isA(), + isA(), + ], + ); + }, + ); +} 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); - }); -}