diff --git a/.fvm/fvm_config.json b/.fvm/fvm_config.json index d8abe1b9..1726d93d 100644 --- a/.fvm/fvm_config.json +++ b/.fvm/fvm_config.json @@ -1,4 +1,3 @@ { - "flutterSdkVersion": "3.13.9", - "flavors": {} + "flutterSdkVersion": "3.13.9" } \ No newline at end of file diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..6108f14a --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.13.9", + "flavors": {} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1be2d875..7040cb04 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,6 @@ app.*.map.json /android/app/release # fvm -.fvm/flutter_sdk \ No newline at end of file + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f285aa4a..2951c6d3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,9 @@ { - "dart.flutterSdkPath": ".fvm/flutter_sdk", - "search.exclude": { - "**/.fvm": true - }, - "files.watcherExclude": { - "**/.fvm": true - } + "dart.flutterSdkPath": ".fvm/versions/3.13.9", + "search.exclude": { + "**/.fvm": true + }, + "files.watcherExclude": { + "**/.fvm": true + } } \ No newline at end of file diff --git a/assets/svg/circle_green.svg b/assets/svg/circle_green.svg new file mode 100644 index 00000000..5900787d --- /dev/null +++ b/assets/svg/circle_green.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/circle_red.svg b/assets/svg/circle_red.svg new file mode 100644 index 00000000..9b4f05dc --- /dev/null +++ b/assets/svg/circle_red.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/svg/star.svg b/assets/svg/star.svg new file mode 100644 index 00000000..87f75959 --- /dev/null +++ b/assets/svg/star.svg @@ -0,0 +1,3 @@ + + + diff --git a/fonts/Lora-Bold.ttf b/fonts/Lora-Bold.ttf new file mode 100644 index 00000000..530c9e11 Binary files /dev/null and b/fonts/Lora-Bold.ttf differ diff --git a/fonts/Lora-Medium.ttf b/fonts/Lora-Medium.ttf new file mode 100644 index 00000000..85ca5a27 Binary files /dev/null and b/fonts/Lora-Medium.ttf differ diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 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 592ceee8..c4855bfe 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 00000000..c236dc0f --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '14.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/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 00000000..68c86a02 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,23 @@ +PODS: + - Flutter (1.0.0) + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - Flutter (from `Flutter`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + +PODFILE CHECKSUM: e60e17f8bfffff789408fce3f968c37c5c63400e + +COCOAPODS: 1.13.0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 73cf3f6d..f1eabcab 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 00B07F0BFD93F3FE48E32742 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F3E3C2A00DC08E71DE335F5 /* Pods_Runner.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; @@ -32,6 +33,8 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4F3E3C2A00DC08E71DE335F5 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 68833FFF5C840B3F5CAEF635 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -42,6 +45,8 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BE1F7C8AF7B78E15497D78F9 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + EF7E9E4C13D86896204E5096 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +54,24 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 00B07F0BFD93F3FE48E32742 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 73CA00C62D685E39639EBEBF /* Pods */ = { + isa = PBXGroup; + children = ( + EF7E9E4C13D86896204E5096 /* Pods-Runner.debug.xcconfig */, + 68833FFF5C840B3F5CAEF635 /* Pods-Runner.release.xcconfig */, + BE1F7C8AF7B78E15497D78F9 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +89,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 73CA00C62D685E39639EBEBF /* Pods */, + D8E6A30210B33D79B7F7D21C /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +117,14 @@ path = Runner; sourceTree = ""; }; + D8E6A30210B33D79B7F7D21C /* Frameworks */ = { + isa = PBXGroup; + children = ( + 4F3E3C2A00DC08E71DE335F5 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +132,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 096D77EFA5C68EB72EFCEE97 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 82260A138AA466E24D5D6B67 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -169,6 +198,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 096D77EFA5C68EB72EFCEE97 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -185,6 +236,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 82260A138AA466E24D5D6B67 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/lib/data/constants.dart b/lib/data/constants.dart new file mode 100644 index 00000000..4b7d77b1 --- /dev/null +++ b/lib/data/constants.dart @@ -0,0 +1,45 @@ +class Urls { + static const String baseUrl = 'https://api.yelp.com'; + static const String apiKey = + 'wfYIpeyetAPJbQYg5ITUE4wxzqCvoEQM5FQyW9Xq4SGJG52vkefWY_Irq9yg_TKpXRYJUgTO48W_fVXReEABY919sT74bHoCAyNH4b0kTe94rmEWFWNo1GjFxUXjZXYx'; + static const String ghrapQLRoute = '/v3/graphql'; + static String getRestaurantsByCity({ + required String city, + required int limit, + required int offset, + }) => + ''' +query getRestaurants { + search(location: "$city", limit: $limit, offset: $offset) { + total + business { + id + name + price + rating + photos + reviews { + id + rating + user { + id + image_url + name + } + text + } + categories { + title + alias + } + hours { + is_open_now + } + location { + formatted_address + } + } + } +} +'''; +} diff --git a/lib/data/datasources/local_data_source.dart b/lib/data/datasources/local_data_source.dart new file mode 100644 index 00000000..d67340a8 --- /dev/null +++ b/lib/data/datasources/local_data_source.dart @@ -0,0 +1,35 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +abstract class LocalDataSource { + Future> getFavorites(); + Future addFavorite(String id); + Future removeFavorite(String id); +} + +const key = 'favorites'; + +class LocalDataSourceImpl implements LocalDataSource { + @override + Future addFavorite(String id) async { + final sharedPreferences = await SharedPreferences.getInstance(); + final favorites = await getFavorites(); + if (favorites.contains(id)) return; + favorites.add(id); + sharedPreferences.setStringList(key, favorites); + } + + @override + Future> getFavorites() async { + final sharedPreferences = await SharedPreferences.getInstance(); + final favorites = sharedPreferences.getStringList(key); + return favorites ?? []; + } + + @override + Future removeFavorite(String id) async { + final sharedPreferences = await SharedPreferences.getInstance(); + final favorites = await getFavorites(); + favorites.remove(id); + sharedPreferences.setStringList(key, favorites); + } +} diff --git a/lib/data/datasources/remote_data_source.dart b/lib/data/datasources/remote_data_source.dart new file mode 100644 index 00000000..47e9cf2d --- /dev/null +++ b/lib/data/datasources/remote_data_source.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:logger/logger.dart'; +import 'package:restaurantour/data/constants.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; + +abstract class RemoteDataSource { + Future getRestaurants(); +} + +class RemoteDataSourceImpl implements RemoteDataSource { + RemoteDataSourceImpl() + : dio = Dio( + BaseOptions( + baseUrl: Urls.baseUrl, + headers: { + 'Authorization': 'Bearer ${Urls.apiKey}', + 'Content-Type': 'application/graphql', + }, + ), + ); + + late Dio dio; + + @override + Future getRestaurants({int offset = 0}) async { + try { + final response = await dio.post>( + Urls.ghrapQLRoute, + data: Urls.getRestaurantsByCity( + city: "Las Vegas", + limit: 20, + offset: offset, + ), + ); + final String json = jsonEncode(response.data); + Logger().i(json); + return RestaurantQueryResult.fromJson(response.data!['data']['search']); + } catch (e) { + Logger().e(e); + return null; + } + } +} diff --git a/lib/models/restaurant.dart b/lib/data/models/restaurant.dart similarity index 75% rename from lib/models/restaurant.dart rename to lib/data/models/restaurant.dart index 87c7aab5..66653c66 100644 --- a/lib/models/restaurant.dart +++ b/lib/data/models/restaurant.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'restaurant.g.dart'; @@ -55,11 +56,13 @@ class Review { final String? id; final int? rating; final User? user; + final String? text; const Review({ this.id, this.rating, this.user, + this.text, }); factory Review.fromJson(Map json) => _$ReviewFromJson(json); @@ -83,7 +86,7 @@ class Location { } @JsonSerializable() -class Restaurant { +class Restaurant extends Equatable { final String? id; final String? name; final String? price; @@ -93,6 +96,7 @@ class Restaurant { final List? hours; final List? reviews; final Location? location; + final bool? isFavorite; const Restaurant({ this.id, @@ -104,8 +108,35 @@ class Restaurant { this.hours, this.reviews, this.location, + this.isFavorite = false, }); + Restaurant copyWith({ + String? id, + String? name, + String? price, + double? rating, + List? photos, + List? categories, + List? hours, + List? reviews, + Location? location, + bool? isFavorite, + }) { + return Restaurant( + id: id ?? this.id, + name: name ?? this.name, + price: price ?? this.price, + rating: rating ?? this.rating, + photos: photos ?? this.photos, + categories: categories ?? this.categories, + hours: hours ?? this.hours, + reviews: reviews ?? this.reviews, + location: location ?? this.location, + isFavorite: isFavorite ?? this.isFavorite, + ); + } + factory Restaurant.fromJson(Map json) => _$RestaurantFromJson(json); @@ -135,6 +166,20 @@ class Restaurant { } return false; } + + @override + List get props => [ + id, + name, + price, + rating, + photos, + categories, + hours, + reviews, + location, + isFavorite, + ]; } @JsonSerializable() diff --git a/lib/models/restaurant.g.dart b/lib/data/models/restaurant.g.dart similarity index 98% rename from lib/models/restaurant.g.dart rename to lib/data/models/restaurant.g.dart index 3ed33f9a..bc0be0dd 100644 --- a/lib/models/restaurant.g.dart +++ b/lib/data/models/restaurant.g.dart @@ -42,12 +42,14 @@ Review _$ReviewFromJson(Map json) => Review( user: json['user'] == null ? null : User.fromJson(json['user'] as Map), + text: json['text'] as String?, ); Map _$ReviewToJson(Review instance) => { 'id': instance.id, 'rating': instance.rating, 'user': instance.user, + 'text': instance.text, }; Location _$LocationFromJson(Map json) => Location( diff --git a/lib/data/repositories/favorite_repository_impl.dart b/lib/data/repositories/favorite_repository_impl.dart new file mode 100644 index 00000000..f298cc01 --- /dev/null +++ b/lib/data/repositories/favorite_repository_impl.dart @@ -0,0 +1,35 @@ +import 'package:restaurantour/data/datasources/local_data_source.dart'; +import 'package:restaurantour/domain/repositories/favorite_repository.dart'; + +class FavoriteRepositoryImpl implements FavoriteRepository { + final LocalDataSource localDataSource; + + FavoriteRepositoryImpl({required this.localDataSource}); + + @override + Future addFavorite(String id) async { + try { + await localDataSource.addFavorite(id); + } on Exception { + throw Exception('Failed to add favorite'); + } + } + + @override + Future removeFavorite(String id) async { + try { + await localDataSource.removeFavorite(id); + } on Exception { + throw Exception('Failed to remove favorite'); + } + } + + @override + Future> getFavorites() async { + try { + return await localDataSource.getFavorites(); + } on Exception { + throw Exception('Failed to get favorites'); + } + } +} diff --git a/lib/data/repositories/restaurants_repository_impl.dart b/lib/data/repositories/restaurants_repository_impl.dart new file mode 100644 index 00000000..68c36e38 --- /dev/null +++ b/lib/data/repositories/restaurants_repository_impl.dart @@ -0,0 +1,20 @@ +import 'dart:io'; + +import 'package:restaurantour/data/datasources/remote_data_source.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/domain/repositories/restaurants_repository.dart'; + +class RestaurantsRepositoryImpl implements RestaurantsRepository { + final RemoteDataSource remoteDataSource; + + RestaurantsRepositoryImpl({required this.remoteDataSource}); + + @override + Future getRestaurants() async { + try { + return await remoteDataSource.getRestaurants(); + } on SocketException { + throw Exception('No internet connection'); + } + } +} diff --git a/lib/domain/repositories/favorite_repository.dart b/lib/domain/repositories/favorite_repository.dart new file mode 100644 index 00000000..be38fe4a --- /dev/null +++ b/lib/domain/repositories/favorite_repository.dart @@ -0,0 +1,5 @@ +abstract class FavoriteRepository { + Future addFavorite(String id); + Future removeFavorite(String id); + Future> getFavorites(); +} diff --git a/lib/domain/repositories/mock/mock_favorite_repository.dart b/lib/domain/repositories/mock/mock_favorite_repository.dart new file mode 100644 index 00000000..ef481e3b --- /dev/null +++ b/lib/domain/repositories/mock/mock_favorite_repository.dart @@ -0,0 +1,22 @@ +import 'package:restaurantour/domain/repositories/favorite_repository.dart'; + +class MockFavoriteRepository implements FavoriteRepository { + final List _favorites = []; + + @override + Future addFavorite(String id) { + _favorites.add(id); + return Future.value(); + } + + @override + Future> getFavorites() { + return Future.value(_favorites); + } + + @override + Future removeFavorite(String id) { + _favorites.remove(id); + return Future.value(); + } +} diff --git a/lib/domain/repositories/mock/mock_restaurants_repository.dart b/lib/domain/repositories/mock/mock_restaurants_repository.dart new file mode 100644 index 00000000..0ad21f0f --- /dev/null +++ b/lib/domain/repositories/mock/mock_restaurants_repository.dart @@ -0,0 +1,16 @@ +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/domain/repositories/restaurants_repository.dart'; + +const mockRestaurants = [ + Restaurant(id: '1', name: 'Restaurant 1'), + Restaurant(id: '2', name: 'Restaurant 2'), + Restaurant(id: '3', name: 'Restaurant 3'), +]; + +class MockRestaurantsRepository implements RestaurantsRepository { + @override + Future getRestaurants() async { + const result = RestaurantQueryResult(restaurants: mockRestaurants); + return result; + } +} diff --git a/lib/domain/repositories/restaurants_repository.dart b/lib/domain/repositories/restaurants_repository.dart new file mode 100644 index 00000000..21655360 --- /dev/null +++ b/lib/domain/repositories/restaurants_repository.dart @@ -0,0 +1,5 @@ +import 'package:restaurantour/data/models/restaurant.dart'; + +abstract class RestaurantsRepository { + Future getRestaurants(); +} diff --git a/lib/domain/usercases/favorites_usercase.dart b/lib/domain/usercases/favorites_usercase.dart new file mode 100644 index 00000000..65fcc3ce --- /dev/null +++ b/lib/domain/usercases/favorites_usercase.dart @@ -0,0 +1,25 @@ +import 'package:restaurantour/domain/repositories/favorite_repository.dart'; + +class FavoritesUsercase { + final FavoriteRepository _favoriteRepository; + + FavoritesUsercase(this._favoriteRepository); + + Future> get() async { + return await _favoriteRepository.getFavorites(); + } + + Future isFavorite(String id) async { + final favorites = await get(); + return favorites.contains(id); + } + + Future toggleFavorite(String id) async { + final isFavorite = await this.isFavorite(id); + if (isFavorite) { + await _favoriteRepository.removeFavorite(id); + } else { + await _favoriteRepository.addFavorite(id); + } + } +} diff --git a/lib/domain/usercases/restaurants_usercase.dart b/lib/domain/usercases/restaurants_usercase.dart new file mode 100644 index 00000000..e5efdfc6 --- /dev/null +++ b/lib/domain/usercases/restaurants_usercase.dart @@ -0,0 +1,12 @@ +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/domain/repositories/restaurants_repository.dart'; + +class RestaurantsUsercase { + final RestaurantsRepository repository; + + RestaurantsUsercase(this.repository); + + Future call() async { + return await repository.getRestaurants(); + } +} diff --git a/lib/injection.dart b/lib/injection.dart new file mode 100644 index 00000000..aad453a8 --- /dev/null +++ b/lib/injection.dart @@ -0,0 +1,37 @@ +// import 'package:dio/dio.dart'; +import 'package:get_it/get_it.dart'; +import 'package:restaurantour/data/datasources/local_data_source.dart'; +import 'package:restaurantour/data/datasources/remote_data_source.dart'; +import 'package:restaurantour/data/repositories/favorite_repository_impl.dart'; +import 'package:restaurantour/data/repositories/restaurants_repository_impl.dart'; +import 'package:restaurantour/domain/repositories/favorite_repository.dart'; +import 'package:restaurantour/domain/repositories/restaurants_repository.dart'; +import 'package:restaurantour/domain/usercases/restaurants_usercase.dart'; +import 'package:restaurantour/domain/usercases/favorites_usercase.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; + +final locator = GetIt.instance; + +void init() { + // bloc + locator.registerFactory(() => RestaurantsBloc(locator(), locator())); + + // usecase + locator.registerLazySingleton(() => RestaurantsUsercase(locator())); + locator.registerLazySingleton(() => FavoritesUsercase(locator())); + + // repository + locator.registerLazySingleton( + () => RestaurantsRepositoryImpl(remoteDataSource: locator()), + ); + locator.registerLazySingleton( + () => FavoriteRepositoryImpl(localDataSource: locator()), + ); + + // data source + locator.registerLazySingleton(() => RemoteDataSourceImpl()); + locator.registerLazySingleton(() => LocalDataSourceImpl()); + + // external + // locator.registerLazySingleton(() => Dio()); +} diff --git a/lib/main.dart b/lib/main.dart index c6ce7473..854387be 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,56 +1,53 @@ import 'package:flutter/material.dart'; -import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_event.dart'; +import 'package:restaurantour/presentation/pages/restaurant_detail_page.dart'; +import 'package:restaurantour/presentation/pages/restaurants_page.dart'; +import 'package:restaurantour/presentation/utils/style_util.dart'; +import 'injection.dart' as di; void main() { + di.init(); runApp(const Restaurantour()); } class Restaurantour extends StatelessWidget { - // This widget is the root of your application. const Restaurantour({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return MaterialApp( - title: 'RestauranTour', - theme: ThemeData( - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: const HomePage(), - ); - } -} - -class HomePage extends StatelessWidget { - const HomePage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Restaurantour'), - ElevatedButton( - child: const Text('Fetch Restaurants'), - onPressed: () async { - final yelpRepo = YelpRepository(); - - try { - final result = await yelpRepo.getRestaurants(); - if (result != null) { - print('Fetched ${result.restaurants!.length} restaurants'); - } else { - print('No restaurants fetched'); - } - } catch (e) { - print('Failed to fetch restaurants: $e'); - } - }, - ), - ], + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => di.locator() + ..add(const FetchRestaurants('Las Vegas')), + ), + ], + child: MaterialApp( + routes: { + '/detail': (context) { + final arguments = ModalRoute.of(context)!.settings.arguments; + final restaurant = arguments as Restaurant; + return RestaurantDetailPage(id: restaurant.id!); + }, + }, + title: 'Restaurantour', + theme: ThemeData( + primaryColor: Colors.white, + colorScheme: const ColorScheme.light( + primary: Colors.black, + secondary: Colors.black, + ), + scaffoldBackgroundColor: const Color(0xFFFAFAFA), + appBarTheme: AppBarTheme( + color: Colors.white, + iconTheme: const IconThemeData(color: Colors.black), + titleTextStyle: StyleUtil.appBarTitle, + ), ), + home: const RestaurantsPage(), ), ); } diff --git a/lib/presentation/bloc/restaurants_bloc.dart b/lib/presentation/bloc/restaurants_bloc.dart new file mode 100644 index 00000000..e3a62fca --- /dev/null +++ b/lib/presentation/bloc/restaurants_bloc.dart @@ -0,0 +1,47 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/domain/usercases/restaurants_usercase.dart'; +import 'package:restaurantour/domain/usercases/favorites_usercase.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_event.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_state.dart'; + +class RestaurantsBloc extends Bloc { + final RestaurantsUsercase _restaurantsUsercase; + final FavoritesUsercase _favoritesUsercase; + + RestaurantsBloc(this._restaurantsUsercase, this._favoritesUsercase) + : super(RestaurantsEmpty()) { + on(_onFetchRestaurants); + on(_onToggleFavorite); + } + + /// Toggles the favorite status of a restaurant + Future _onToggleFavorite(event, emit) async { + await _favoritesUsercase.toggleFavorite(event.id); + final currentState = state; + if (currentState is RestaurantsLoaded) { + final restaurants = currentState.restaurants.map( + (restaurant) { + if (restaurant.id != event.id) return restaurant; + return restaurant.copyWith(isFavorite: !restaurant.isFavorite!); + }, + ).toList(); + emit(RestaurantsLoaded([...restaurants])); + } + } + + /// Fetches the list of restaurants + Future _onFetchRestaurants(event, emit) async { + emit(RestaurantsLoading()); + + final result = await _restaurantsUsercase.call(); + if (result != null) { + final favs = await _favoritesUsercase.get(); + final restaurants = result.restaurants! + .map((r) => r.copyWith(isFavorite: favs.contains(r.id))) + .toList(); + emit(RestaurantsLoaded([...restaurants])); + } else { + emit(const RestaurantsError("Failed to fetch restaurants")); + } + } +} diff --git a/lib/presentation/bloc/restaurants_event.dart b/lib/presentation/bloc/restaurants_event.dart new file mode 100644 index 00000000..6b4910e0 --- /dev/null +++ b/lib/presentation/bloc/restaurants_event.dart @@ -0,0 +1,24 @@ +import 'package:equatable/equatable.dart'; + +abstract class RestaurantsEvent extends Equatable { + const RestaurantsEvent(); + + @override + List get props => []; +} + +class FetchRestaurants extends RestaurantsEvent { + const FetchRestaurants(this.city); + final String city; + + @override + List get props => [city]; +} + +class ToggleFavorite extends RestaurantsEvent { + const ToggleFavorite(this.id); + final String id; + + @override + List get props => [id]; +} diff --git a/lib/presentation/bloc/restaurants_state.dart b/lib/presentation/bloc/restaurants_state.dart new file mode 100644 index 00000000..115e10e6 --- /dev/null +++ b/lib/presentation/bloc/restaurants_state.dart @@ -0,0 +1,31 @@ +import 'package:equatable/equatable.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; + +abstract class RestaurantsState extends Equatable { + const RestaurantsState(); + + @override + List get props => []; +} + +class RestaurantsEmpty extends RestaurantsState {} + +class RestaurantsLoading extends RestaurantsState {} + +class RestaurantsError extends RestaurantsState { + final String message; + + const RestaurantsError(this.message); + + @override + List get props => [message]; +} + +class RestaurantsLoaded extends RestaurantsState { + final List restaurants; + + const RestaurantsLoaded(this.restaurants); + + @override + List get props => [restaurants]; +} diff --git a/lib/presentation/pages/restaurant_detail_page.dart b/lib/presentation/pages/restaurant_detail_page.dart new file mode 100644 index 00000000..969aea8a --- /dev/null +++ b/lib/presentation/pages/restaurant_detail_page.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_event.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_state.dart'; +import 'package:restaurantour/presentation/utils/iterable_extensions.dart'; +import 'package:restaurantour/presentation/widgets/category_widget.dart'; +import 'package:restaurantour/presentation/widgets/detail_section.dart'; +import 'package:restaurantour/presentation/widgets/divider_widget.dart'; +import 'package:restaurantour/presentation/widgets/open_status_widget.dart'; +import 'package:restaurantour/presentation/widgets/review_widget.dart'; +import 'package:restaurantour/presentation/widgets/stars_widget.dart'; + +class RestaurantDetailPage extends StatelessWidget { + final String id; + + const RestaurantDetailPage({Key? key, required this.id}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is RestaurantsLoaded) { + final restaurant = + state.restaurants.firstWhereOrNull((r) => r.id == id); + + if (restaurant == null) { + Navigator.of(context).pop(); + return const SizedBox.shrink(); + } + + return Scaffold( + appBar: AppBar( + title: Text(restaurant.name ?? 'Restaurant Details'), + actions: [ + IconButton( + onPressed: () { + context + .read() + .add(ToggleFavorite(restaurant.id!)); + }, + icon: Icon( + restaurant.isFavorite! + ? Icons.favorite + : Icons.favorite_border, + ), + ), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + Hero( + tag: restaurant.id!, + child: Image.network( + restaurant.heroImage, + height: 375, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CategoryWidget( + price: restaurant.price, + category: restaurant.displayCategory, + ), + Expanded(child: Container()), + OpenStatusWidget(isOpen: restaurant.isOpen), + ], + ), + const SizedBox(height: 12), + const DividerWidget(), + DetailSection( + title: 'Address', + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Text( + restaurant.location!.formattedAddress!, + style: + const TextStyle(fontWeight: FontWeight.w600), + ), + ), + ), + const DividerWidget(), + DetailSection( + title: 'Overall Rating', + child: StarsWidget.large(rating: restaurant.rating!), + ), + const DividerWidget(), + DetailSection( + title: '${restaurant.reviews!.length} Reviews', + child: ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: restaurant.reviews!.length, + itemBuilder: (_, i) => + ReviewWidget(review: restaurant.reviews![i]), + separatorBuilder: (_, __) => const DividerWidget(), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + return const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ); + }, + ); + } +} diff --git a/lib/presentation/pages/restaurants_page.dart b/lib/presentation/pages/restaurants_page.dart new file mode 100644 index 00000000..36ec3003 --- /dev/null +++ b/lib/presentation/pages/restaurants_page.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_event.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_state.dart'; +import 'package:restaurantour/presentation/widgets/restaurant_tile.dart'; + +class RestaurantsPage extends StatefulWidget { + const RestaurantsPage({Key? key}) : super(key: key); + + @override + State createState() => _RestaurantsPageState(); +} + +class _RestaurantsPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('RestauranTour'), + bottom: TabBar( + controller: _tabController, + isScrollable: true, + indicatorColor: Colors.black, + indicatorSize: TabBarIndicatorSize.label, + tabs: const [ + Tab(text: 'All Restaurants'), + Tab(text: 'My Favorites'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + BlocBuilder( + builder: (context, state) { + if (state is RestaurantsLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (state is RestaurantsEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('RestauranTour'), + ElevatedButton( + child: const Text('Fetch Restaurants!'), + onPressed: () async { + context + .read() + .add(const FetchRestaurants("Las Vegas")); + }, + ), + ], + ), + ); + } + if (state is RestaurantsLoaded) { + return ListView.builder( + padding: const EdgeInsets.only( + top: 12, + left: 6, + right: 6, + bottom: 100, + ), + itemCount: state.restaurants.length, + itemBuilder: (context, index) { + final restaurant = state.restaurants[index]; + return RestaurantTile(restaurant: restaurant); + }, + ); + } + return const Center(child: Text('Error')); + }, + ), + BlocBuilder( + builder: (context, state) { + if (state is RestaurantsLoading) { + return const Center(child: CircularProgressIndicator()); + } + if (state is RestaurantsEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('RestauranTour'), + ElevatedButton( + child: const Text('Fetch Restaurants!'), + onPressed: () async { + context + .read() + .add(const FetchRestaurants("Las Vegas")); + }, + ), + ], + ), + ); + } + if (state is RestaurantsLoaded) { + var favs = + state.restaurants.where((r) => r.isFavorite!).toList(); + if (favs.isEmpty) { + return const Center(child: Text('No favorites yet')); + } + return ListView.builder( + padding: const EdgeInsets.only( + top: 12, + left: 6, + right: 6, + bottom: 100, + ), + itemCount: favs.length, + itemBuilder: (context, index) { + final restaurant = favs[index]; + return RestaurantTile(restaurant: restaurant); + }, + ); + } + return const Center(child: Text('Error')); + }, + ), + ], + ), + ); + } +} diff --git a/lib/presentation/utils/color_util.dart b/lib/presentation/utils/color_util.dart new file mode 100644 index 00000000..1cee274e --- /dev/null +++ b/lib/presentation/utils/color_util.dart @@ -0,0 +1,7 @@ +import 'dart:ui'; + +class ColorUtil { + static Color backgroundColor = const Color(0xFFFAFAFA); + static Color defaultText = const Color(0xFF000000); + static Color dividerLine = const Color(0xFFEEEEEE); +} diff --git a/lib/presentation/utils/iterable_extensions.dart b/lib/presentation/utils/iterable_extensions.dart new file mode 100644 index 00000000..26b8696c --- /dev/null +++ b/lib/presentation/utils/iterable_extensions.dart @@ -0,0 +1,9 @@ +extension IterableExtensions on Iterable { + T? firstWhereOrNull(bool Function(T element) comparator) { + try { + return firstWhere(comparator); + } on StateError catch (_) { + return null; + } + } +} diff --git a/lib/presentation/utils/style_util.dart b/lib/presentation/utils/style_util.dart new file mode 100644 index 00000000..be8fa03f --- /dev/null +++ b/lib/presentation/utils/style_util.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class StyleUtil { + static TextStyle tileTitle = const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + fontFamily: 'Lora', + ); + static TextStyle rating = const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + fontFamily: 'Lora', + ); + static TextStyle appBarTitle = const TextStyle( + color: Colors.black, + fontFamily: 'Lora', + fontSize: 18, + fontWeight: FontWeight.bold, + ); +} diff --git a/lib/presentation/widgets/address_widget.dart b/lib/presentation/widgets/address_widget.dart new file mode 100644 index 00000000..a5fe30c6 --- /dev/null +++ b/lib/presentation/widgets/address_widget.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class AddressWidget extends StatelessWidget { + final String address; + + const AddressWidget({Key? key, required this.address}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + const Text('Address', style: TextStyle(fontSize: 12)), + const SizedBox(height: 24), + Text(address, style: const TextStyle(fontSize: 16)), + const SizedBox(height: 12), + ], + ); + } +} diff --git a/lib/presentation/widgets/category_widget.dart b/lib/presentation/widgets/category_widget.dart new file mode 100644 index 00000000..6aeed237 --- /dev/null +++ b/lib/presentation/widgets/category_widget.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class CategoryWidget extends StatelessWidget { + // final Restaurant restaurant; + + final String? price; + final String category; + + const CategoryWidget({Key? key, this.price, required this.category}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + if (price != null) Text(price!, style: const TextStyle(fontSize: 12)), + if (price != null) const SizedBox(width: 4), + Text(category, style: const TextStyle(fontSize: 12)), + ], + ); + } +} diff --git a/lib/presentation/widgets/detail_section.dart b/lib/presentation/widgets/detail_section.dart new file mode 100644 index 00000000..a5727085 --- /dev/null +++ b/lib/presentation/widgets/detail_section.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class DetailSection extends StatelessWidget { + final String title; + final Widget child; + + const DetailSection({ + Key? key, + required this.title, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Text(title, style: const TextStyle(fontSize: 12)), + const SizedBox(height: 16), + child, + const SizedBox(height: 12), + ], + ); + } +} diff --git a/lib/presentation/widgets/divider_widget.dart b/lib/presentation/widgets/divider_widget.dart new file mode 100644 index 00000000..f3f39ccd --- /dev/null +++ b/lib/presentation/widgets/divider_widget.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/presentation/utils/color_util.dart'; + +class DividerWidget extends StatelessWidget { + const DividerWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Divider( + color: ColorUtil.dividerLine, + height: 24, + thickness: 1, + ); + } +} diff --git a/lib/presentation/widgets/open_status_widget.dart b/lib/presentation/widgets/open_status_widget.dart new file mode 100644 index 00000000..a1ed2fa5 --- /dev/null +++ b/lib/presentation/widgets/open_status_widget.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class OpenStatusWidget extends StatelessWidget { + final bool isOpen; + + const OpenStatusWidget({Key? key, required this.isOpen}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text( + isOpen ? 'Open Now' : 'Closed', + style: const TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + ), + ), + const SizedBox(width: 4), + SvgPicture.asset( + isOpen ? 'assets/svg/circle_green.svg' : 'assets/svg/circle_red.svg', + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/restaurant_tile.dart b/lib/presentation/widgets/restaurant_tile.dart new file mode 100644 index 00000000..61f68a82 --- /dev/null +++ b/lib/presentation/widgets/restaurant_tile.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/presentation/utils/style_util.dart'; +import 'package:restaurantour/presentation/widgets/category_widget.dart'; +import 'package:restaurantour/presentation/widgets/open_status_widget.dart'; +import 'package:restaurantour/presentation/widgets/stars_widget.dart'; + +class RestaurantTile extends StatelessWidget { + final Restaurant restaurant; + + const RestaurantTile({Key? key, required this.restaurant}) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + '/detail', + arguments: restaurant, + ); + }, + child: Container( + constraints: const BoxConstraints(maxHeight: 104), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.12), + blurRadius: 5, + offset: const Offset(0, 1), + ), + ], + ), + margin: const EdgeInsets.all(6), + padding: const EdgeInsets.all(8), + child: Row( + children: [ + Hero( + tag: restaurant.id!, + child: Image.network( + restaurant.heroImage, + frameBuilder: (_, child, __, ___) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: child, + ); + }, + width: 88, + height: 88, + fit: BoxFit.cover, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 2), + Expanded( + child: Text( + restaurant.name!, + maxLines: 2, + style: StyleUtil.tileTitle, + ), + ), + const SizedBox(height: 4), + CategoryWidget( + category: restaurant.displayCategory, + price: restaurant.price, + ), + const SizedBox(height: 4), + Row( + children: [ + StarsWidget(rating: restaurant.rating!), + Expanded(child: Container()), + OpenStatusWidget(isOpen: restaurant.isOpen), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/review_widget.dart b/lib/presentation/widgets/review_widget.dart new file mode 100644 index 00000000..0d6fc3ca --- /dev/null +++ b/lib/presentation/widgets/review_widget.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/data/models/restaurant.dart'; +import 'package:restaurantour/presentation/widgets/stars_widget.dart'; + +class ReviewWidget extends StatelessWidget { + final Review review; + + const ReviewWidget({Key? key, required this.review}) : super(key: key); + + List _buildText() { + if (review.text == null) return []; + return [ + Text(review.text!, style: const TextStyle(fontSize: 16)), + const SizedBox(height: 8), + ]; + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + StarsWidget(rating: review.rating!.toDouble()), + const SizedBox(height: 8), + ..._buildText(), + Row( + children: [ + CircleAvatar( + child: Text(review.user!.name![0]), + radius: 20, + foregroundImage: NetworkImage(review.user!.imageUrl ?? ''), + ), + const SizedBox(width: 8), + Text(review.user!.name ?? '', style: const TextStyle(fontSize: 12)), + ], + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/stars_widget.dart b/lib/presentation/widgets/stars_widget.dart new file mode 100644 index 00000000..370aa589 --- /dev/null +++ b/lib/presentation/widgets/stars_widget.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:restaurantour/presentation/utils/style_util.dart'; + +class StarsWidget extends StatelessWidget { + final double rating; + + const StarsWidget({Key? key, required this.rating}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: List.generate( + rating.floor(), + (index) => SvgPicture.asset('assets/svg/star.svg'), + ), + ); + } + + static large({required double rating}) { + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(rating.toString(), style: StyleUtil.rating), + Padding( + padding: const EdgeInsets.only(left: 3, bottom: 7), + child: SvgPicture.asset('assets/svg/star.svg'), + ), + ], + ); + } +} diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart deleted file mode 100644 index f251d7b4..00000000 --- a/lib/repositories/yelp_repository.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; -import 'package:restaurantour/models/restaurant.dart'; - -const _apiKey = ''; - -class YelpRepository { - late Dio dio; - - YelpRepository({ - @visibleForTesting Dio? dio, - }) : dio = dio ?? - Dio( - BaseOptions( - baseUrl: 'https://api.yelp.com', - headers: { - 'Authorization': 'Bearer $_apiKey', - 'Content-Type': 'application/graphql', - }, - ), - ); - - /// Returns a response in this shape - /// { - /// "data": { - /// "search": { - /// "total": 5056, - /// "business": [ - /// { - /// "id": "faPVqws-x-5k2CQKDNtHxw", - /// "name": "Yardbird Southern Table & Bar", - /// "price": "$$", - /// "rating": 4.5, - /// "photos": [ - /// "https:///s3-media4.fl.yelpcdn.com/bphoto/_zXRdYX4r1OBfF86xKMbDw/o.jpg" - /// ], - /// "reviews": [ - /// { - /// "id": "sjZoO8wcK1NeGJFDk5i82Q", - /// "rating": 5, - /// "user": { - /// "id": "BuBCkWFNT_O2dbSnBZvpoQ", - /// "image_url": "https:///s3-media2.fl.yelpcdn.com/photo/v8tbTjYaFvkzh1d7iE-pcQ/o.jpg", - /// "name": "Gina T." - /// } - /// }, - /// { - /// "id": "okpO9hfpxQXssbTZTKq9hA", - /// "rating": 5, - /// "user": { - /// "id": "0x9xu_b0Ct_6hG6jaxpztw", - /// "image_url": "https:///s3-media3.fl.yelpcdn.com/photo/gjz8X6tqE3e4praK4HfCiA/o.jpg", - /// "name": "Crystal L." - /// } - /// }, - /// ... - /// ] - /// } - /// } - /// - Future getRestaurants({int offset = 0}) async { - try { - final response = await dio.post>( - '/v3/graphql', - data: _getQuery(offset), - ); - return RestaurantQueryResult.fromJson(response.data!['data']['search']); - } catch (e) { - return null; - } - } - - String _getQuery(int offset) { - return ''' -query getRestaurants { - search(location: "Las Vegas", limit: 20, offset: $offset) { - total - business { - id - name - price - rating - photos - reviews { - id - rating - user { - id - image_url - name - } - } - categories { - title - alias - } - hours { - is_open_now - } - location { - formatted_address - } - } - } -} -'''; - } -} diff --git a/pubspec.lock b/pubspec.lock index 0b052c68..c4a733f1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: args - sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" async: dependency: transitive description: @@ -33,6 +33,22 @@ 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" + bloc_test: + dependency: "direct main" + description: + name: bloc_test + sha256: "165a6ec950d9252ebe36dc5335f2e6eb13055f33d56db0eeb7642768849b43d2" + url: "https://pub.dev" + source: hosted + version: "9.1.7" boolean_selector: dependency: transitive description: @@ -45,10 +61,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -61,34 +77,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.9" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f4d6244cc071ba842c296cb1c4ee1b31596b9f924300647ac7a1445493471a3f + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.3" + version: "7.3.0" built_collection: dependency: transitive description: @@ -101,10 +117,10 @@ packages: dependency: transitive description: name: built_value - sha256: b6c9911b2d670376918d5b8779bc27e0e612a94ec3ff0343689e991d8d0a3b8a + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.1.4" + version: "8.9.2" characters: dependency: transitive description: @@ -113,22 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 - url: "https://pub.dev" - source: hosted - version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: dd007e4fb8270916820a0d66e24f619266b60773cddd082c6439341645af2659 + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" clock: dependency: transitive description: @@ -149,26 +157,34 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.2" convert: dependency: transitive description: name: convert - sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" + url: "https://pub.dev" + source: hosted + version: "1.6.4" crypto: dependency: transitive description: name: crypto - sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -185,14 +201,30 @@ 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: name: dio - sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.3+1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" fake_async: dependency: transitive description: @@ -201,27 +233,43 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" file: dependency: transitive description: name: file - sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "7.0.0" fixnum: dependency: transitive description: name: fixnum - sha256: "6a2ef17156f4dc49684f9d99aaf4a93aba8ac49f5eac861755f5730ddf6e2e4e" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_bloc: + dependency: "direct main" + description: + name: flutter_bloc + sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2 + url: "https://pub.dev" + source: hosted + version: "8.1.5" flutter_lints: dependency: "direct dev" description: @@ -234,15 +282,20 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -251,14 +304,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 + url: "https://pub.dev" + source: hosted + version: "7.6.7" glob: dependency: transitive description: name: glob - sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" graphs: dependency: transitive description: @@ -267,38 +328,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + http: + dependency: transitive + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: bfb651625e251a88804ad6d596af01ea903544757906addcb2dcdf088b5ea185 + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" io: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174 + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.6.7" json_annotation: dependency: "direct main" description: @@ -323,14 +392,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: "8c94b8c219e7e50194efc8771cd0e9f10807d8d3e219af473d89b06cc2ee4e04" + url: "https://pub.dev" + source: hosted + version: "2.2.0" logging: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" matcher: dependency: transitive description: @@ -351,26 +428,50 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.9.1" mime: dependency: transitive description: name: mime - sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" + mocktail: + dependency: transitive + description: + name: mocktail + sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + 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: name: package_config - sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: transitive description: @@ -387,54 +488,174 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + 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: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" petitparser: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "3.1.4" + 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: name: pool - sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" pub_semver: dependency: transitive description: name: pub_semver - sha256: b5a5fcc6425ea43704852ba4453ba94b08c2226c63418a260240c3a054579014 + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "3686efe4a4613a4449b1a4ae08670aadbd3376f2e78d93e3f8f0919db02a7256" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.3" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" shelf: dependency: transitive description: name: shelf - sha256: c240984c924796e055e831a0a36db23be8cb04f170b26df572931ab36418421d + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" + shelf_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: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: fd84910bf7d58db109082edf7326b75322b8f186162028482f53dc892f00332d + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -456,6 +677,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.4" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + 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: @@ -468,26 +705,26 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -504,54 +741,70 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + url: "https://pub.dev" + source: hosted + version: "1.24.3" test_api: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.5.3" timing: dependency: transitive description: name: timing - sha256: c386d07d7f5efc613479a7c4d9d64b03710b03cfaa7e8ad5f2bfb295a1f0dfad + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.2" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -560,46 +813,78 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + url: "https://pub.dev" + source: hosted + version: "11.10.0" watcher: dependency: transitive description: name: watcher - sha256: e42dfcc48f67618344da967b10f62de57e04bae01d9d3af4c2596f3712a88c99 + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" web: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.4.0" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + win32: + dependency: transitive + description: + name: win32 + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" xml: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.3.0" yaml: dependency: transitive description: name: yaml - sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.7.0-0" + dart: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index be3055e0..14aa17fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,9 +13,15 @@ dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.6 - dio: ^5.4.0 json_annotation: ^4.8.1 - flutter_svg: ^2.0.9 + flutter_svg: ^2.0.10+1 + get_it: ^7.6.7 + equatable: ^2.0.5 + flutter_bloc: ^8.1.5 + logger: ^2.2.0 + shared_preferences: ^2.2.3 + dio: ^5.4.3+1 + bloc_test: ^9.1.7 dev_dependencies: flutter_test: @@ -26,5 +32,12 @@ dev_dependencies: flutter: uses-material-design: true -# assets: -# - assets/svg/ \ No newline at end of file + fonts: + - family: Lora + fonts: + - asset: fonts/Lora-Bold.ttf + weight: 700 + - asset: fonts/Lora-Medium.ttf + weight: 500 + assets: + - assets/svg/ \ No newline at end of file diff --git a/test/bloc_test.dart b/test/bloc_test.dart new file mode 100644 index 00000000..0d1947a4 --- /dev/null +++ b/test/bloc_test.dart @@ -0,0 +1,40 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurantour/domain/repositories/mock/mock_favorite_repository.dart'; +import 'package:restaurantour/domain/repositories/mock/mock_restaurants_repository.dart'; +import 'package:restaurantour/domain/usercases/favorites_usercase.dart'; +import 'package:restaurantour/domain/usercases/restaurants_usercase.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_bloc.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_event.dart'; +import 'package:restaurantour/presentation/bloc/restaurants_state.dart'; + +void main() { + group('Restaurants Test', () { + late RestaurantsBloc restaurantsBloc; + + setUp(() { + EquatableConfig.stringify = true; + final mockRestaurantsUsercase = RestaurantsUsercase( + MockRestaurantsRepository(), + ); + final mockFavoritesUsercase = FavoritesUsercase(MockFavoriteRepository()); + restaurantsBloc = RestaurantsBloc( + mockRestaurantsUsercase, + mockFavoritesUsercase, + ); + }); + + blocTest( + 'get restaurants', + build: () => restaurantsBloc, + act: (bloc) => bloc.add(const FetchRestaurants("Las Vegas")), + expect: () => [ + RestaurantsLoading(), + const RestaurantsLoaded(mockRestaurants), + ], + ); + + tearDown(() => restaurantsBloc.close()); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart index 83fbeae4..7a9b5f85 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,20 +1,33 @@ -// 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/material.dart'; import 'package:flutter_test/flutter_test.dart'; - -import 'package:restaurantour/main.dart'; +import 'package:restaurantour/presentation/widgets/address_widget.dart'; void main() { - testWidgets('Page loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const Restaurantour()); + testWidgets( + 'AddressWidget displays correct title and address', + (WidgetTester tester) async { + // Build the AddressWidget with a specific address. + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: AddressWidget( + address: '123 Main St', + ), + ), + ), + ); - // Verify that tests will run - expect(find.text('Fetch Restaurants'), findsOneWidget); - }); + // Verify that the AddressWidget contains the correct title and address. + expect( + find.text('Address'), + findsOneWidget, + reason: 'Title is not found', + ); + expect( + find.text('123 Main St'), + findsOneWidget, + reason: 'Address is not found', + ); + }, + ); }