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..69968622 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,9 @@ app.*.map.json /android/app/release # fvm -.fvm/flutter_sdk \ No newline at end of file + +# FVM Version Cache +.fvm/ + +# Hide .env file with sensitive data +.env \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index f285aa4a..30ecbd75 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,3 @@ { - "dart.flutterSdkPath": ".fvm/flutter_sdk", - "search.exclude": { - "**/.fvm": true - }, - "files.watcherExclude": { - "**/.fvm": true - } + "dart.flutterSdkPath": ".fvm/flutter_sdk" } \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index e47cb81d..48e4ccd6 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 34 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/android/build.gradle b/android/build.gradle index 24047dce..f7eb7f63 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.7.10' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.3.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index bc6a58af..6b665338 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip diff --git a/assets/img/no_image_available.png b/assets/img/no_image_available.png new file mode 100644 index 00000000..35d46606 Binary files /dev/null and b/assets/img/no_image_available.png 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..3c6dbb6d --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.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..97899b13 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,23 @@ +PODS: + - Flutter (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - Flutter (from `Flutter`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + +PODFILE CHECKSUM: aa3b1d9cb94e8055dc6468141196cf9e4c8e33df + +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 73cf3f6d..41ff9fdf 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 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 */; }; + 8C6D6A104316E422A501D715 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 162C7F7578E5A65825E052A8 /* Pods_Runner.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -31,7 +32,9 @@ /* Begin PBXFileReference section */ 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 = ""; }; + 162C7F7578E5A65825E052A8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4C16E82A7CA9A912FAFCE439 /* 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 = ""; }; 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 = ""; }; + A136471F75D3CE0B14C99D01 /* 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 = ""; }; + BB48F4533F02225DA383EAD3 /* 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +54,21 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 8C6D6A104316E422A501D715 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 44BC54F666F081B02849A7BA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 162C7F7578E5A65825E052A8 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +86,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + BAD0884EF99AD91421FC0403 /* Pods */, + 44BC54F666F081B02849A7BA /* Frameworks */, ); sourceTree = ""; }; @@ -98,6 +114,17 @@ path = Runner; sourceTree = ""; }; + BAD0884EF99AD91421FC0403 /* Pods */ = { + isa = PBXGroup; + children = ( + 4C16E82A7CA9A912FAFCE439 /* Pods-Runner.debug.xcconfig */, + A136471F75D3CE0B14C99D01 /* Pods-Runner.release.xcconfig */, + BB48F4533F02225DA383EAD3 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -105,12 +132,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 295363677098BBFDE82EEA4C /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 47CEE88DEFDE431696C4B91F /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -169,6 +198,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 295363677098BBFDE82EEA4C /* [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"; }; + 47CEE88DEFDE431696C4B91F /* [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/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard index f3c28516..228e24f9 100644 --- a/ios/Runner/Base.lproj/Main.storyboard +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -1,8 +1,10 @@ - - + + + - + + @@ -14,13 +16,14 @@ - + - + + diff --git a/lib/constants/text_style_constants.dart b/lib/constants/text_style_constants.dart new file mode 100644 index 00000000..6dfb0626 --- /dev/null +++ b/lib/constants/text_style_constants.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class TextStylesClass { + //Lora's font common text styles + //basic text style for titles + static final TextStyle titleTextStyle = + GoogleFonts.lora(fontSize: 18, fontWeight: FontWeight.bold); + + //basic text style for titles + static final TextStyle restaurantCardTitleTextStyle = + GoogleFonts.lora(fontSize: 16, fontWeight: FontWeight.w500); + + //Open Sans' font common text styles + //basic text style for titles + static final TextStyle subtitlesTextStyle = + GoogleFonts.openSans(fontSize: 16, fontWeight: FontWeight.w600); + + static final TextStyle priceCategoryTextStyle = + GoogleFonts.openSans(fontSize: 12); + + static final TextStyle captionTextStyle = + GoogleFonts.openSans(fontSize: 12, fontWeight: FontWeight.w400); + + static final TextStyle openCloseRestaurantTextStyle = GoogleFonts.openSans( + fontStyle: FontStyle.italic, + fontWeight: FontWeight.w400, + fontSize: 12, + ); + + static final TextStyle reviewRestaurantTextStyle = GoogleFonts.openSans( + fontSize: 16, + fontWeight: FontWeight.w400, + ); + + static final TextStyle restaurantAddressTextStyle = GoogleFonts.openSans( + fontSize: 14, + fontWeight: FontWeight.w600, + ); +} diff --git a/lib/main.dart b/lib/main.dart index c6ce7473..6acd684a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,57 +1,26 @@ import 'package:flutter/material.dart'; -import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -void main() { - runApp(const Restaurantour()); +import 'package:restaurantour/screens/restaurants_screen.dart'; +import 'package:restaurantour/theme/main_theme.dart'; + +void main() async { + WidgetsFlutterBinding + .ensureInitialized(); //Ensure flutter before loading .env file + await dotenv.load(); //Load the sensitive data with .env + runApp(const ProviderScope(child: 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'); - } - }, - ), - ], - ), - ), + home: const RestaurantListScreen(), + theme: MainTheme.mainTheme, ); } } diff --git a/lib/providers/favorite_restaurants_provider.dart b/lib/providers/favorite_restaurants_provider.dart new file mode 100644 index 00000000..0afa6bfd --- /dev/null +++ b/lib/providers/favorite_restaurants_provider.dart @@ -0,0 +1,21 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:restaurantour/models/restaurant.dart'; + +class FavoritesNotifier extends StateNotifier> { + FavoritesNotifier() : super([]); + + void addFavorite(Restaurant restaurant) { + if (!state.contains(restaurant)) { + state = [...state, restaurant]; + } + } + + void removeFavorite(Restaurant restaurant) { + state = state.where((r) => r.id != restaurant.id).toList(); + } +} + +final favoritesProvider = + StateNotifierProvider>((ref) { + return FavoritesNotifier(); +}); diff --git a/lib/providers/fetch_restaurants_provider.dart b/lib/providers/fetch_restaurants_provider.dart new file mode 100644 index 00000000..ff260e65 --- /dev/null +++ b/lib/providers/fetch_restaurants_provider.dart @@ -0,0 +1,48 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/repositories/yelp_repository.dart'; + +// YelpRepository provider +final yelpRepositoryProvider = Provider((ref) { + return YelpRepository(); +}); + +//Restuaurants Provider +class RestaurantsNotifier extends StateNotifier>> { + final YelpRepository repository; + + RestaurantsNotifier(this.repository) : super(const AsyncValue.loading()) { + fetchRestaurantsIfNeeded(); + } + + Future fetchRestaurantsIfNeeded() async { + if (state is! AsyncData) { + fetchRestaurants(); + } + } + + Future fetchRestaurants() async { + try { + final result = await repository.getRestaurants(); + if (result != null && result.restaurants != null) { + state = AsyncValue.data(result.restaurants!); + } else { + state = AsyncValue.error( + Exception("No restaurants found"), + StackTrace.current, + ); + } + } catch (e) { + state = AsyncValue.error( + Exception('Failed to fetch restaurants: $e'), + StackTrace.current, + ); + } + } +} + +final restaurantsNotifierProvider = + StateNotifierProvider>>( + (ref) { + return RestaurantsNotifier(ref.watch(yelpRepositoryProvider)); +}); diff --git a/lib/repositories/yelp_repository.dart b/lib/repositories/yelp_repository.dart index f251d7b4..bfe78108 100644 --- a/lib/repositories/yelp_repository.dart +++ b/lib/repositories/yelp_repository.dart @@ -1,8 +1,10 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; + import 'package:restaurantour/models/restaurant.dart'; -const _apiKey = ''; +//const _apiKey = ''; class YelpRepository { late Dio dio; @@ -14,7 +16,7 @@ class YelpRepository { BaseOptions( baseUrl: 'https://api.yelp.com', headers: { - 'Authorization': 'Bearer $_apiKey', + 'Authorization': 'Bearer ${dotenv.env['API_KEY']}', 'Content-Type': 'application/graphql', }, ), diff --git a/lib/screens/restaurant_detail_screen.dart b/lib/screens/restaurant_detail_screen.dart new file mode 100644 index 00000000..f80a40f1 --- /dev/null +++ b/lib/screens/restaurant_detail_screen.dart @@ -0,0 +1,251 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:collection/collection.dart'; +import 'package:gap/gap.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:restaurantour/constants/text_style_constants.dart'; +import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/providers/favorite_restaurants_provider.dart'; +import 'package:restaurantour/providers/fetch_restaurants_provider.dart'; +import 'package:restaurantour/utils/rating_calculator.dart'; +import 'package:restaurantour/widgets/review_card_widget.dart'; + +class RestaurantDetailScreen extends ConsumerStatefulWidget { + const RestaurantDetailScreen({Key? key, required this.restaurantId}) + : super(key: key); + + final String restaurantId; + + @override + _RestaurantDetailScreenState createState() => _RestaurantDetailScreenState(); +} + +class _RestaurantDetailScreenState + extends ConsumerState { + // State variables + late List reviews; + late bool isFavorite = false; + + @override + void initState() { + super.initState(); + initRestaurantDetails(); // Fetch and process restaurant details + } + + // Fetches and processes restaurant details + void initRestaurantDetails() async { + reviews = []; //variable where the reviews will be managed + final restaurants = ref.read(restaurantsNotifierProvider).asData?.value; + // Check if restaurants data is available + if (restaurants != null) { + // Find restaurant by ID + final restaurant = restaurants + .firstWhereOrNull((rest) => rest.id == widget.restaurantId); + if (restaurant != null) { + // Check favorite status + isFavorite = ref.read(favoritesProvider).contains(restaurant); + if (restaurant.reviews != null) { + populateReviewsList(restaurant.reviews!); // Populate reviews list + } + } else { + throw Exception("Restaurant with ID ${widget.restaurantId} not found"); + } + } + } + + // Toggles restaurant as favorites + void toggleFavorite(Restaurant restaurant) { + final favoritesNotifier = ref.read(favoritesProvider.notifier); + if (isFavorite) { + favoritesNotifier.removeFavorite(restaurant); + } else { + favoritesNotifier.addFavorite(restaurant); + } + setState(() { + isFavorite = !isFavorite; + }); + } + + // Populates the reviews list with ReviewCardWidget widgets + void populateReviewsList(List fetchedReviews) { + for (Review review in fetchedReviews) { + reviews.add( + ReviewCardWidget( + rating: review.rating, + username: review.user?.name, + userImgUrl: review.user?.imageUrl, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + final restaurantState = ref.watch(restaurantsNotifierProvider); + + return restaurantState.when( + // Handle loading state + loading: () => const CircularProgressIndicator( + color: Colors.black, + ), + // Handle error state + error: (error, _) => Text('Error: $error'), + // Handle data state + data: (restaurants) { + final restaurant = restaurants + .firstWhereOrNull((rest) => rest.id == widget.restaurantId); + if (restaurant == null) { + return const Text("Restaurant not found"); + } + return buildRestaurantDetails(restaurant); + }, + ); + } + + // Builds the UI for the restaurant details screen + Widget buildRestaurantDetails(Restaurant restaurant) { + //Obtain the overall rating for the current restaurant + double overallRating = + RatingCalculator.calculateAverageRating(restaurant.reviews!); + + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon( + Icons.arrow_back, + color: Colors.black, + ), + ), + centerTitle: false, + title: Text( + restaurant.name ?? 'No restaurant name found', + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + actions: [ + IconButton( + onPressed: () => toggleFavorite(restaurant), + icon: Icon( + isFavorite ? Icons.favorite : Icons.favorite_border_outlined, + color: Colors.red, + ), + ), + ], + ), + body: ListView( + children: [ + Container( + width: double.infinity, + height: 361, + decoration: BoxDecoration( + image: DecorationImage( + image: restaurant.heroImage.isNotEmpty + ? NetworkImage( + restaurant.heroImage, + ) + : const AssetImage('assets/img/no_image_available.png') + as ImageProvider, + fit: BoxFit.cover, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${restaurant.price ?? 'N/A'} ${restaurant.categories?.isNotEmpty == true ? restaurant.categories!.first.title : 'N/A'}', + style: TextStylesClass.priceCategoryTextStyle, + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + restaurant.isOpen == true ? 'Open Now' : 'Closed', + style: TextStylesClass.openCloseRestaurantTextStyle, + ), + const Gap(6), + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + restaurant.isOpen ? Colors.green : Colors.red, + ), + ), + ], + ), + ], + ), + const Gap(24), + const Divider(), + const Gap(24), + Text( + 'Address', + style: TextStylesClass.captionTextStyle, + ), + const Gap(24), + Text( + restaurant.location?.formattedAddress ?? + 'No addressed provided', + style: TextStylesClass.restaurantAddressTextStyle, + ), + const Gap(24), + const Divider(), + const Gap(24), + Text( + 'Overall Rating', + style: TextStylesClass.captionTextStyle, + ), + const Gap(24), + Row( + children: [ + Text( + //Check if there are any reviews + (overallRating != 0.0 && restaurant.reviews!.isNotEmpty) + ? '$overallRating' + : 'No Reviews', + style: GoogleFonts.lora( + fontWeight: FontWeight.w700, + fontSize: 28, + ), + ), + const Icon( + Icons.star, + color: Colors.amber, + ), + ], + ), + const Gap(24), + const Divider(), + const Gap(24), + Text( + '${reviews.length} Reviews', + style: TextStylesClass.captionTextStyle, + ), + const Gap(24), + //show/loop through reviews - except the last one + for (int i = 0; i < reviews.length - 1; i++) ...[ + reviews[i], + const Divider(), + ], + + //show the last review (without the divider at the end) + reviews.isNotEmpty ? reviews.last : Container(), + const Gap(100), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/restaurants_screen.dart b/lib/screens/restaurants_screen.dart new file mode 100644 index 00000000..59bf6670 --- /dev/null +++ b/lib/screens/restaurants_screen.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:restaurantour/constants/text_style_constants.dart'; +import 'package:restaurantour/screens/views/all_restaurants_view.dart'; +import 'package:restaurantour/screens/views/favorite_restaurants_view.dart'; + +class RestaurantListScreen extends ConsumerWidget { + const RestaurantListScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return DefaultTabController( + // This controls the tab behavior (initial index, number of tabs) + initialIndex: 0, + length: 2, + child: Scaffold( + appBar: AppBar( + // Configure app bar appearance + backgroundColor: Colors.white, + centerTitle: true, + title: Text( + 'Restaurantour', + style: TextStylesClass.titleTextStyle, + ), + bottom: TabBar( + // Configure tab layout and appearance + tabAlignment: TabAlignment.center, + indicatorColor: Colors.black, + tabs: [ + Tab( + child: Text( + 'All Restaurants', + style: TextStylesClass.subtitlesTextStyle, + ), + ), + Tab( + child: Text( + 'My Favorites', + style: TextStylesClass.subtitlesTextStyle, + ), + ), + ], + ), + ), + body: const TabBarView( + children: [ + //All restaurants list view + AllRestaurantsView(), + //Favorite restaurants list view + FavoriteRestaurantsView(), + ], + ), + ), + ); + } +} diff --git a/lib/screens/views/all_restaurants_view.dart b/lib/screens/views/all_restaurants_view.dart new file mode 100644 index 00000000..b90592e6 --- /dev/null +++ b/lib/screens/views/all_restaurants_view.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:restaurantour/providers/fetch_restaurants_provider.dart'; +import 'package:restaurantour/widgets/restaurant_item_widget.dart'; + +// This widget displays a list of all restaurants +class AllRestaurantsView extends ConsumerWidget { + const AllRestaurantsView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Access the current state of the restaurantsNotifierProvider + final restaurantsState = ref.watch(restaurantsNotifierProvider); + + return restaurantsState.when( + // Handle loading state + loading: () => const Center( + child: CircularProgressIndicator( + color: Colors.black, + ), + ), + // Handle error state + error: (error, stack) => + const Center(child: Text('Error: No restaurants found')), + // Handle data state and build the list + data: (restaurants) { + return ListView.builder( + padding: const EdgeInsets.only(bottom: 100, top: 12), + itemCount: restaurants.length, + // Build each restaurant item using BuildRestaurantItem widget + itemBuilder: (context, index) => BuildRestaurantItem( + restaurantData: restaurants[index], + ), + ); + }, + ); + } +} diff --git a/lib/screens/views/favorite_restaurants_view.dart b/lib/screens/views/favorite_restaurants_view.dart new file mode 100644 index 00000000..30a03837 --- /dev/null +++ b/lib/screens/views/favorite_restaurants_view.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:restaurantour/providers/favorite_restaurants_provider.dart'; +import 'package:restaurantour/widgets/restaurant_item_widget.dart'; + +// This widget displays a list of favorite restaurants +class FavoriteRestaurantsView extends ConsumerWidget { + const FavoriteRestaurantsView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Access the current list of favorite restaurants from the provider + final favoriteRestaurantsList = ref.watch(favoritesProvider); + + // Display a message if there are no favorite restaurants + if (favoriteRestaurantsList.isEmpty) { + return const Center( + child: Text('No favorite restaurants yet'), + ); + } + + return ListView.builder( + padding: const EdgeInsets.only(bottom: 100, top: 12), + itemCount: favoriteRestaurantsList.length, + // Build each restaurant item using BuildRestaurantItem widget + itemBuilder: (context, index) => BuildRestaurantItem( + restaurantData: favoriteRestaurantsList[index], + ), + ); + } +} diff --git a/lib/theme/main_theme.dart b/lib/theme/main_theme.dart new file mode 100644 index 00000000..6580ea24 --- /dev/null +++ b/lib/theme/main_theme.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +//Basic color theme +class MainTheme { + static ThemeData get mainTheme { + const Color primaryColor = Color(0xFFFB8C00); + const Color secondaryColor = Color(0xFF4CAF50); + const Color errorColor = Colors.red; + const Color primaryTextColor = Colors.black; + + return ThemeData.from( + colorScheme: const ColorScheme( + primary: primaryColor, + secondary: secondaryColor, + surface: Colors.white, + background: Colors.white, + error: errorColor, + onPrimary: Colors.white, + onSecondary: Colors.white, + onSurface: primaryTextColor, + onBackground: primaryTextColor, + onError: Colors.white, + brightness: Brightness.light, + ), + ).copyWith( + appBarTheme: AppBarTheme( + color: Colors.white, + titleTextStyle: GoogleFonts.lora( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + iconTheme: const IconThemeData( + color: primaryTextColor, + ), + buttonTheme: const ButtonThemeData( + buttonColor: primaryColor, + textTheme: ButtonTextTheme.primary, + ), + ); + } +} diff --git a/lib/utils/rating_calculator.dart b/lib/utils/rating_calculator.dart new file mode 100644 index 00000000..5ebe30ae --- /dev/null +++ b/lib/utils/rating_calculator.dart @@ -0,0 +1,16 @@ +import 'package:restaurantour/models/restaurant.dart'; + +//Get overall rating for a restaurant / based on reviews +class RatingCalculator { + static double calculateAverageRating(List reviews) { + if (reviews.isNotEmpty) { + double average = reviews + .map((review) => review.rating?.toDouble() ?? 0.0) + .reduce((a, b) => a + b) / + reviews.length; + + return double.parse(average.toStringAsFixed(2)); + } + return 0.0; // Return 0 if there are no reviews or reviews are null + } +} diff --git a/lib/widgets/restaurant_card_widget.dart b/lib/widgets/restaurant_card_widget.dart new file mode 100644 index 00000000..b6c02511 --- /dev/null +++ b/lib/widgets/restaurant_card_widget.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:restaurantour/constants/text_style_constants.dart'; +import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/utils/rating_calculator.dart'; + +class RestaurantCardWidget extends StatelessWidget { + const RestaurantCardWidget({Key? key, required this.restaurantData}) + : super(key: key); + final Restaurant restaurantData; + + @override + Widget build(BuildContext context) { + //Get overall rating + final int overallRating = restaurantData.reviews?.isNotEmpty ?? false + ? RatingCalculator.calculateAverageRating(restaurantData.reviews!) + .round() + : 0; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: SizedBox( + width: double.infinity, + child: Container( + height: 104, + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.5), + blurRadius: 2, + offset: const Offset(0, 1), + ), + ], + ), + child: Row( + children: [ + Container( + width: 88, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + //Display a dummy image for restaurants with no image + image: restaurantData.heroImage.isNotEmpty + ? NetworkImage(restaurantData.heroImage) + : const AssetImage('assets/img/no_image_available.png') + as ImageProvider, + fit: BoxFit.cover, + ), + ), + ), + const Gap(10), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + restaurantData.name ?? 'No restaurant name found', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStylesClass.restaurantCardTitleTextStyle, + ), + const Gap(8), + Text( + //general null checks + '${restaurantData.price ?? 'N/A'} ${restaurantData.categories?.isNotEmpty == true ? restaurantData.categories!.first.title : "N/A"}', + style: TextStylesClass.priceCategoryTextStyle, + ), + const Gap(6), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + //generate the stars depending on the overall rating + children: List.generate( + overallRating, + (index) => const Icon( + Icons.star, + color: Colors.amber, + size: 14, + ), + ), + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + restaurantData.isOpen == true + ? 'Open Now' + : 'Closed', + style: + TextStylesClass.openCloseRestaurantTextStyle, + ), + const Gap(6), + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: restaurantData.isOpen + ? Colors.green + : Colors.red, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/restaurant_item_widget.dart b/lib/widgets/restaurant_item_widget.dart new file mode 100644 index 00000000..d53fdf88 --- /dev/null +++ b/lib/widgets/restaurant_item_widget.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/screens/restaurant_detail_screen.dart'; +import 'package:restaurantour/widgets/restaurant_card_widget.dart'; + +class BuildRestaurantItem extends StatelessWidget { + const BuildRestaurantItem({ + Key? key, + required this.restaurantData, + }) : super(key: key); + final Restaurant restaurantData; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + //Navigate to the restaurant's details screen on tap + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => RestaurantDetailScreen( + //In case no id is given (might never happen) + restaurantId: restaurantData.id ?? 'noId', + ), + ), + ); + }, + //Widget used to build UI + child: RestaurantCardWidget( + restaurantData: restaurantData, + ), + ); + } +} diff --git a/lib/widgets/review_card_widget.dart b/lib/widgets/review_card_widget.dart new file mode 100644 index 00000000..a217f57f --- /dev/null +++ b/lib/widgets/review_card_widget.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_lorem/flutter_lorem.dart'; +import 'package:gap/gap.dart'; +import 'package:restaurantour/constants/text_style_constants.dart'; + +class ReviewCardWidget extends StatelessWidget { + const ReviewCardWidget({ + Key? key, + this.rating = 0, + this.username = "User name", + this.userImgUrl, + }) : super(key: key); + final int? rating; + final String? username; + final String? userImgUrl; + + @override + Widget build(BuildContext context) { + //Generate lorem ipsum text for reviews + String dummyReviewText = lorem(paragraphs: 1, words: 15); + //Round the overall rating (for stars generation) + int roundedRating = rating!.round(); + return Column( + children: [ + const Gap(8), + Row( + children: List.generate( + roundedRating, + (index) => const Icon( + Icons.star, + color: Colors.amber, + size: 14, + ), + ), + ), + const Gap(12), + Text( + dummyReviewText, + style: TextStylesClass.reviewRestaurantTextStyle, + ), + const Gap(12), + Row( + children: [ + CircleAvatar( + //Use dummy user image if userImgUrl is null + backgroundImage: NetworkImage( + userImgUrl ?? + 'https://gopostr.s3.amazonaws.com/favicon_url/CMXfauwVNmmVLyKpV0Qkg582dzzQWcp0Eje9gMiQ.png', + ), + ), + const Gap(8), + Text( + username!, + style: TextStylesClass.captionTextStyle, + ), + ], + ), + const Gap(16), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 0b052c68..dd9883a7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,14 +17,22 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d + url: "https://pub.dev" + source: hosted + version: "0.11.2" args: dependency: transitive description: name: args - sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" async: dependency: transitive description: @@ -77,10 +85,10 @@ packages: 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: @@ -129,6 +137,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" + source: hosted + version: "0.4.1" clock: dependency: transitive description: @@ -146,13 +170,13 @@ packages: source: hosted version: "4.10.0" collection: - dependency: transitive + dependency: "direct main" 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: @@ -165,10 +189,10 @@ packages: 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: @@ -177,6 +201,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + custom_lint: + dependency: "direct dev" + description: + name: custom_lint + sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799" + url: "https://pub.dev" + source: hosted + version: "0.6.4" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 + url: "https://pub.dev" + source: hosted + version: "0.6.3" dart_style: dependency: transitive description: @@ -201,14 +241,22 @@ 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: @@ -222,6 +270,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" + url: "https://pub.dev" + source: hosted + version: "5.1.0" flutter_lints: dependency: "direct dev" description: @@ -230,6 +286,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + flutter_lorem: + dependency: "direct main" + description: + name: flutter_lorem + sha256: "004e1af5b030b632628d8cc1eb2f90451f48b6c0080f43069f69baa4959e9fe5" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "4bce556b7ecbfea26109638d5237684538d4abc509d253e6c5c4c5733b360098" + url: "https://pub.dev" + source: hosted + version: "2.4.10" flutter_svg: dependency: "direct main" description: @@ -243,6 +315,14 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" frontend_server_client: dependency: transitive description: @@ -251,14 +331,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.0" + gap: + dependency: "direct main" + description: + name: gap + sha256: f19387d4e32f849394758b91377f9153a1b41d79513ef7668c088c77dbc6955d + url: "https://pub.dev" + source: hosted + version: "3.0.1" glob: dependency: transitive description: name: glob - sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: f0b8d115a13ecf827013ec9fc883390ccc0e87a96ed5347a3114cac177ef18e8 + url: "https://pub.dev" + source: hosted + version: "6.1.0" graphs: dependency: transitive description: @@ -267,6 +363,14 @@ 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: @@ -351,10 +455,10 @@ 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: @@ -367,10 +471,10 @@ packages: dependency: transitive description: name: package_config - sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: transitive description: @@ -387,14 +491,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "51f0d2c554cfbc9d6a312ab35152fc77e2f0b758ce9f1a444a3a1e5b8f3c6b7f" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + 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: "6.0.2" + version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + 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: @@ -407,18 +575,34 @@ packages: dependency: transitive description: name: pub_semver - sha256: b5a5fcc6425ea43704852ba4453ba94b08c2226c63418a260240c3a054579014 + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "3686efe4a4613a4449b1a4ae08670aadbd3376f2e78d93e3f8f0919db02a7256" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.3" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "548e2192eb7aeb826eb89387f814edb76594f3363e2c0bb99dd733d795ba3589" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" shelf: dependency: transitive description: @@ -464,22 +648,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.0.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: @@ -508,10 +708,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.0" timing: dependency: transitive description: @@ -528,6 +728,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f" + url: "https://pub.dev" + source: hosted + version: "4.2.2" vector_graphics: dependency: transitive description: @@ -572,10 +780,10 @@ packages: 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: @@ -584,22 +792,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + 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..84eefb0e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,30 +1,37 @@ name: restaurantour description: Flutter developer coding challenge starter project. -publish_to: 'none' +publish_to: "none" version: 1.0.0+1 - environment: sdk: ">=2.12.0 <3.0.0" dependencies: flutter: sdk: flutter + collection: ^1.15.0 cupertino_icons: ^1.0.6 dio: ^5.4.0 json_annotation: ^4.8.1 flutter_svg: ^2.0.9 + flutter_dotenv: ^5.1.0 + google_fonts: 6.1.0 + gap: ^3.0.1 + flutter_riverpod: ^2.4.10 + flutter_lorem: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^1.0.2 - build_runner: ^2.4.8 + build_runner: ^2.4.9 json_serializable: ^6.7.1 + custom_lint: ^0.6.4 flutter: uses-material-design: true -# assets: -# - assets/svg/ \ No newline at end of file + assets: + - .env + - assets/img/ diff --git a/test/provider/favorite_restaurants_test.dart b/test/provider/favorite_restaurants_test.dart new file mode 100644 index 00000000..ebd20763 --- /dev/null +++ b/test/provider/favorite_restaurants_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurantour/models/restaurant.dart'; +import 'package:restaurantour/providers/favorite_restaurants_provider.dart'; + +void main() { + group('FavoritesNotifier', () { + test('should add a restaurant to favorites when not already added', () { + final container = ProviderContainer(); + final notifier = container.read(favoritesProvider.notifier); + const testRestaurant = Restaurant(id: '1', name: 'Test Restaurant'); + + notifier.addFavorite(testRestaurant); + + expect( + container.read(favoritesProvider), + contains(testRestaurant), + ); + }); + + test('should not add a duplicate restaurant to favorites', () { + final container = ProviderContainer(); + final notifier = container.read(favoritesProvider.notifier); + const testRestaurant = Restaurant(id: '1', name: 'Test Restaurant'); + + notifier.addFavorite(testRestaurant); + notifier.addFavorite(testRestaurant); // Attempt to add it again + + expect( + container.read(favoritesProvider).length, + 1, + ); + }); + + test('should remove a restaurant from favorites', () { + final container = ProviderContainer(); + final notifier = container.read(favoritesProvider.notifier); + const testRestaurant = Restaurant(id: '1', name: 'Test Restaurant'); + + notifier.addFavorite(testRestaurant); + notifier.removeFavorite(testRestaurant); + + expect( + container.read(favoritesProvider), + isNot( + contains(testRestaurant), + ), + ); + }); + }); +} diff --git a/test/provider/widget_test.dart b/test/provider/widget_test.dart new file mode 100644 index 00000000..b2da90cd --- /dev/null +++ b/test/provider/widget_test.dart @@ -0,0 +1,29 @@ +// ignore_for_file: prefer_const_constructors +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:restaurantour/models/restaurant.dart'; + +import 'package:restaurantour/widgets/restaurant_card_widget.dart'; +import 'package:restaurantour/widgets/restaurant_item_widget.dart'; + +void main() { + testWidgets('BuildRestaurantItem displays restaurant name', + (WidgetTester tester) async { + // Mock restaurant data + final mockRestaurant = Restaurant(id: '1', name: 'Test Restaurant'); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: BuildRestaurantItem( + restaurantData: mockRestaurant, + ), + ), + ), + ); + + // Verify that the RestaurantCardWidget is displaying the correct name + expect(find.byType(RestaurantCardWidget), findsOneWidget); + expect(find.text('Test Restaurant'), findsOneWidget); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 83fbeae4..00000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,20 +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:restaurantour/main.dart'; - -void main() { - testWidgets('Page loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const Restaurantour()); - - // Verify that tests will run - expect(find.text('Fetch Restaurants'), findsOneWidget); - }); -}