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/.metadata b/.metadata index 0f055bf1..faee0409 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,27 @@ # This file should be version controlled and should not be manually edited. version: - revision: ffb2ecea5223acdd139a5039be2f9c796962833d - channel: stable + revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d" + channel: "stable" project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: android + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/android/app/build.gradle b/android/app/build.gradle index e47cb81d..944b4e58 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,12 +22,10 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { - compileSdkVersion flutter.compileSdkVersion + namespace "com.example.restaurantour" + compileSdk flutter.compileSdkVersion + ndkVersion flutter.ndkVersion compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -44,6 +43,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.restaurantour" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() @@ -63,6 +64,4 @@ flutter { source '../..' } -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} +dependencies {} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 53df105c..399f6981 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,6 +1,6 @@ - - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6cb1ae9c..1e520aca 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - - + @@ -31,4 +30,15 @@ android:name="flutterEmbedding" android:value="2" /> + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/restaurantour/MainActivity.kt b/android/app/src/main/kotlin/com/example/restaurantour/MainActivity.kt index ee0b1427..c677ec60 100644 --- a/android/app/src/main/kotlin/com/example/restaurantour/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/restaurantour/MainActivity.kt @@ -2,5 +2,4 @@ package com.example.restaurantour import io.flutter.embedding.android.FlutterActivity -class MainActivity: FlutterActivity() { -} +class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml index 3db14bb5..06952be7 100644 --- a/android/app/src/main/res/values-night/styles.xml +++ b/android/app/src/main/res/values-night/styles.xml @@ -3,7 +3,7 @@ diff --git a/android/build.gradle b/android/build.gradle index 24047dce..bc157bd1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.3.50' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() @@ -26,6 +13,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/android/gradle.properties b/android/gradle.properties index 94adc3a3..598d13fe 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index bc6a58af..e1ca574e 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Jun 23 08:50:38 CEST 2017 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.6.3-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index 44e62bcf..1d6d19b7 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,11 +1,26 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.7.10" apply false +} + +include ":app" diff --git a/assets/fonts/Lora-Bold.ttf b/assets/fonts/Lora-Bold.ttf new file mode 100644 index 00000000..530c9e11 Binary files /dev/null and b/assets/fonts/Lora-Bold.ttf differ diff --git a/assets/fonts/Lora-BoldItalic.ttf b/assets/fonts/Lora-BoldItalic.ttf new file mode 100644 index 00000000..6bcc76b0 Binary files /dev/null and b/assets/fonts/Lora-BoldItalic.ttf differ diff --git a/assets/fonts/Lora-Italic.ttf b/assets/fonts/Lora-Italic.ttf new file mode 100644 index 00000000..d93bc5fc Binary files /dev/null and b/assets/fonts/Lora-Italic.ttf differ diff --git a/assets/fonts/Lora-Medium.ttf b/assets/fonts/Lora-Medium.ttf new file mode 100644 index 00000000..85ca5a27 Binary files /dev/null and b/assets/fonts/Lora-Medium.ttf differ diff --git a/assets/fonts/Lora-MediumItalic.ttf b/assets/fonts/Lora-MediumItalic.ttf new file mode 100644 index 00000000..42208fbe Binary files /dev/null and b/assets/fonts/Lora-MediumItalic.ttf differ diff --git a/assets/fonts/Lora-Regular.ttf b/assets/fonts/Lora-Regular.ttf new file mode 100644 index 00000000..2b1dab45 Binary files /dev/null and b/assets/fonts/Lora-Regular.ttf differ diff --git a/assets/fonts/Lora-SemiBold.ttf b/assets/fonts/Lora-SemiBold.ttf new file mode 100644 index 00000000..3a7c6d75 Binary files /dev/null and b/assets/fonts/Lora-SemiBold.ttf differ diff --git a/assets/fonts/Lora-SemiBoldItalic.ttf b/assets/fonts/Lora-SemiBoldItalic.ttf new file mode 100644 index 00000000..16c8254d Binary files /dev/null and b/assets/fonts/Lora-SemiBoldItalic.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..d97f17e2 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 00000000..15338f2d --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart diff --git a/lib/core/dependecy_injection.dart b/lib/core/dependecy_injection.dart new file mode 100644 index 00000000..1f4936f0 --- /dev/null +++ b/lib/core/dependecy_injection.dart @@ -0,0 +1,19 @@ +import 'package:dio/dio.dart'; +import 'package:get_it/get_it.dart'; + +final _getIt = GetIt.instance; + +const _apiKey = '<< YOUR API KEY >>'; + +void initDependencies() { + final _dio = Dio( + BaseOptions( + baseUrl: 'https://api.yelp.com', + headers: { + 'Authorization': 'Bearer $_apiKey', + 'Content-Type': 'application/graphql', + }, + ), + ); + _getIt.registerFactory(() => _dio); +} diff --git a/lib/core/design_system/restaurantour_design_system.dart b/lib/core/design_system/restaurantour_design_system.dart new file mode 100644 index 00000000..f7012985 --- /dev/null +++ b/lib/core/design_system/restaurantour_design_system.dart @@ -0,0 +1,2 @@ +export './restaurantour_sizes.dart'; +export './text_styles/restaurantour_text_styles.dart'; diff --git a/lib/core/design_system/restaurantour_sizes.dart b/lib/core/design_system/restaurantour_sizes.dart new file mode 100644 index 00000000..d90fdd7d --- /dev/null +++ b/lib/core/design_system/restaurantour_sizes.dart @@ -0,0 +1,14 @@ +class RestaurantourSizes { + static const size0 = 0.0; + static const size1 = 2.0; + static const size2 = 4.0; + static const size3 = 8.0; + static const size4 = 12.0; + static const size5 = 16.0; + static const size6 = 24.0; + static const size7 = 32.0; + static const size8 = 48.0; + static const size9 = 64.0; + static const size10 = 96.0; + static const size11 = 128.0; +} diff --git a/lib/core/design_system/text_styles/restaurantour_text_styles.dart b/lib/core/design_system/text_styles/restaurantour_text_styles.dart new file mode 100644 index 00000000..af13a17b --- /dev/null +++ b/lib/core/design_system/text_styles/restaurantour_text_styles.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class RestaurantourTextStyles { + static const subtitle1 = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black, + fontFamily: 'Lora', + ); + + static const caption = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: Colors.black, + fontFamily: 'Open sans', + ); + + static const overline = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w400, + color: Colors.black, + fontFamily: 'Open sans', + fontStyle: FontStyle.italic, + ); + + static const body1 = TextStyle( + fontSize: 16, + color: Colors.black, + fontFamily: 'Open sans', + ); + + static const heading6 = TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: Colors.black, + fontFamily: 'Lora', + ); + + static const button = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.black, + fontFamily: 'Open Sans', + ); +} diff --git a/lib/core/design_system/widgets/open_status_widget.dart b/lib/core/design_system/widgets/open_status_widget.dart new file mode 100644 index 00000000..db2ff125 --- /dev/null +++ b/lib/core/design_system/widgets/open_status_widget.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/core/design_system/restaurantour_design_system.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class OpenStatus extends StatelessWidget { + const OpenStatus({Key? key, required this.isOpen}) : super(key: key); + + final bool isOpen; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Text( + isOpen + ? AppLocalizations.of(context)!.openNow + : AppLocalizations.of(context)!.closed, + style: RestaurantourTextStyles.overline, + ), + const SizedBox(width: RestaurantourSizes.size3), + Icon( + Icons.circle, + color: isOpen ? Colors.green : Colors.red, + size: RestaurantourSizes.size4, + ), + ], + ); + } +} diff --git a/lib/core/design_system/widgets/rating_widget.dart b/lib/core/design_system/widgets/rating_widget.dart new file mode 100644 index 00000000..ea424117 --- /dev/null +++ b/lib/core/design_system/widgets/rating_widget.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/core/design_system/restaurantour_sizes.dart'; + +class RatingWidget extends StatelessWidget { + const RatingWidget({Key? key, required this.rating}) : super(key: key); + + final double rating; + + @override + Widget build(BuildContext context) { + return Row( + children: List.generate(5, (index) { + return Icon( + index < (rating).round() ? Icons.star : Icons.star_border, + color: Colors.amber, + size: RestaurantourSizes.size5, + ); + }), + ); + } +} diff --git a/lib/core/hive/hive.dart b/lib/core/hive/hive.dart new file mode 100644 index 00000000..34f65d2b --- /dev/null +++ b/lib/core/hive/hive.dart @@ -0,0 +1,7 @@ +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; + +Future initHive() async { + final dir = await getApplicationDocumentsDirectory(); + Hive.init(dir.path); +} diff --git a/lib/core/hive/hive_type_id.dart b/lib/core/hive/hive_type_id.dart new file mode 100644 index 00000000..224eac01 --- /dev/null +++ b/lib/core/hive/hive_type_id.dart @@ -0,0 +1,6 @@ +const categoryTypeId = 0; +const hoursTypeId = 1; +const userTypeId = 2; +const reviewTypeId = 3; +const restaurantTypeId = 4; +const locationTypeId = 5; diff --git a/lib/features/auth/.gitkeep b/lib/features/auth/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/lib/features/profile/.gitkeep b/lib/features/profile/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/lib/features/restaurant/data/local_restaurant_datasource.dart b/lib/features/restaurant/data/local_restaurant_datasource.dart new file mode 100644 index 00000000..710a48b0 --- /dev/null +++ b/lib/features/restaurant/data/local_restaurant_datasource.dart @@ -0,0 +1,35 @@ +import 'package:hive/hive.dart'; +import 'package:restaurantour/features/restaurant/domain/models/restaurant.dart'; + +abstract class LocalRestaurantDataSource { + Future> getFavoriteRestaurants(); + Future insertFavoriteRestaurant(Restaurant restaurant); + Future deleteFavoriteRestaurant(Restaurant restaurant); +} + +class LocalRestaurantDataSourceImpl extends LocalRestaurantDataSource { + static const _favoriteBoxName = 'favorite_restaurants'; + LocalRestaurantDataSourceImpl(); + + @override + Future> getFavoriteRestaurants() async { + final box = Hive.box(_favoriteBoxName); + return box.values.toList(); + } + + @override + Future insertFavoriteRestaurant(Restaurant restaurant) async { + Hive.box(_favoriteBoxName).add( + restaurant, + ); + } + + @override + Future deleteFavoriteRestaurant(Restaurant restaurant) async { + final box = Hive.box(_favoriteBoxName); + final index = box.values + .toList() + .indexWhere((element) => element.id == restaurant.id); + box.deleteAt(index); + } +} diff --git a/lib/features/restaurant/data/remote_restaurant_datasource.dart b/lib/features/restaurant/data/remote_restaurant_datasource.dart new file mode 100644 index 00000000..2efdbf52 --- /dev/null +++ b/lib/features/restaurant/data/remote_restaurant_datasource.dart @@ -0,0 +1,61 @@ +import 'package:dio/dio.dart'; +import 'package:get_it/get_it.dart'; + +abstract class RemoteRestaurantDataSource { + Future>> getRestaurants(int offset); +} + +class RemoteRestaurantDataSourceImpl extends RemoteRestaurantDataSource { + RemoteRestaurantDataSourceImpl(); + + final _dioClient = GetIt.instance(); + + @override + Future>> getRestaurants(int offset) async { + try { + return await _dioClient.post>( + '/v3/graphql', + data: _getQuery(offset), + ); + } catch (e) { + rethrow; + } + } + + 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 + text + user { + id + image_url + name + } + } + categories { + title + alias + } + hours { + is_open_now + } + location { + formatted_address + } + } + } +} +'''; + } +} diff --git a/lib/models/restaurant.dart b/lib/features/restaurant/domain/models/restaurant.dart similarity index 83% rename from lib/models/restaurant.dart rename to lib/features/restaurant/domain/models/restaurant.dart index 87c7aab5..22237b88 100644 --- a/lib/models/restaurant.dart +++ b/lib/features/restaurant/domain/models/restaurant.dart @@ -1,10 +1,15 @@ +import 'package:hive/hive.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:restaurantour/core/hive/hive_type_id.dart'; part 'restaurant.g.dart'; @JsonSerializable() +@HiveType(typeId: categoryTypeId) class Category { + @HiveField(0) final String? alias; + @HiveField(1) final String? title; Category({ @@ -19,8 +24,10 @@ class Category { } @JsonSerializable() +@HiveType(typeId: hoursTypeId) class Hours { @JsonKey(name: 'is_open_now') + @HiveField(0) final bool? isOpenNow; const Hours({ @@ -33,10 +40,14 @@ class Hours { } @JsonSerializable() +@HiveType(typeId: userTypeId) class User { + @HiveField(0) final String? id; @JsonKey(name: 'image_url') + @HiveField(1) final String? imageUrl; + @HiveField(2) final String? name; const User({ @@ -51,15 +62,22 @@ class User { } @JsonSerializable() +@HiveType(typeId: reviewTypeId) class Review { + @HiveField(0) final String? id; + @HiveField(1) final int? rating; + @HiveField(2) final User? user; + @HiveField(3) + final String? text; const Review({ this.id, this.rating, this.user, + this.text, }); factory Review.fromJson(Map json) => _$ReviewFromJson(json); @@ -68,8 +86,10 @@ class Review { } @JsonSerializable() +@HiveType(typeId: 5) class Location { @JsonKey(name: 'formatted_address') + @HiveField(0) final String? formattedAddress; Location({ @@ -83,15 +103,25 @@ class Location { } @JsonSerializable() +@HiveType(typeId: restaurantTypeId) class Restaurant { + @HiveField(0) final String? id; + @HiveField(1) final String? name; + @HiveField(2) final String? price; + @HiveField(3) final double? rating; + @HiveField(4) final List? photos; + @HiveField(5) final List? categories; + @HiveField(6) final List? hours; + @HiveField(7) final List? reviews; + @HiveField(8) final Location? location; const Restaurant({ diff --git a/lib/features/restaurant/domain/models/restaurant.g.dart b/lib/features/restaurant/domain/models/restaurant.g.dart new file mode 100644 index 00000000..82b3d127 --- /dev/null +++ b/lib/features/restaurant/domain/models/restaurant.g.dart @@ -0,0 +1,361 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'restaurant.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class CategoryAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + Category read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Category( + alias: fields[0] as String?, + title: fields[1] as String?, + ); + } + + @override + void write(BinaryWriter writer, Category obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.alias) + ..writeByte(1) + ..write(obj.title); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CategoryAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class HoursAdapter extends TypeAdapter { + @override + final int typeId = 1; + + @override + Hours read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Hours( + isOpenNow: fields[0] as bool?, + ); + } + + @override + void write(BinaryWriter writer, Hours obj) { + writer + ..writeByte(1) + ..writeByte(0) + ..write(obj.isOpenNow); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is HoursAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class UserAdapter extends TypeAdapter { + @override + final int typeId = 2; + + @override + User read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return User( + id: fields[0] as String?, + imageUrl: fields[1] as String?, + name: fields[2] as String?, + ); + } + + @override + void write(BinaryWriter writer, User obj) { + writer + ..writeByte(3) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.imageUrl) + ..writeByte(2) + ..write(obj.name); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class ReviewAdapter extends TypeAdapter { + @override + final int typeId = 3; + + @override + Review read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Review( + id: fields[0] as String?, + rating: fields[1] as int?, + user: fields[2] as User?, + text: fields[3] as String?, + ); + } + + @override + void write(BinaryWriter writer, Review obj) { + writer + ..writeByte(4) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.rating) + ..writeByte(2) + ..write(obj.user) + ..writeByte(3) + ..write(obj.text); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ReviewAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class LocationAdapter extends TypeAdapter { + @override + final int typeId = 5; + + @override + Location read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Location( + formattedAddress: fields[0] as String?, + ); + } + + @override + void write(BinaryWriter writer, Location obj) { + writer + ..writeByte(1) + ..writeByte(0) + ..write(obj.formattedAddress); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is LocationAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +class RestaurantAdapter extends TypeAdapter { + @override + final int typeId = 4; + + @override + Restaurant read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Restaurant( + id: fields[0] as String?, + name: fields[1] as String?, + price: fields[2] as String?, + rating: fields[3] as double?, + photos: (fields[4] as List?)?.cast(), + categories: (fields[5] as List?)?.cast(), + hours: (fields[6] as List?)?.cast(), + reviews: (fields[7] as List?)?.cast(), + location: fields[8] as Location?, + ); + } + + @override + void write(BinaryWriter writer, Restaurant obj) { + writer + ..writeByte(9) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.name) + ..writeByte(2) + ..write(obj.price) + ..writeByte(3) + ..write(obj.rating) + ..writeByte(4) + ..write(obj.photos) + ..writeByte(5) + ..write(obj.categories) + ..writeByte(6) + ..write(obj.hours) + ..writeByte(7) + ..write(obj.reviews) + ..writeByte(8) + ..write(obj.location); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is RestaurantAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Category _$CategoryFromJson(Map json) => Category( + alias: json['alias'] as String?, + title: json['title'] as String?, + ); + +Map _$CategoryToJson(Category instance) => { + 'alias': instance.alias, + 'title': instance.title, + }; + +Hours _$HoursFromJson(Map json) => Hours( + isOpenNow: json['is_open_now'] as bool?, + ); + +Map _$HoursToJson(Hours instance) => { + 'is_open_now': instance.isOpenNow, + }; + +User _$UserFromJson(Map json) => User( + id: json['id'] as String?, + imageUrl: json['image_url'] as String?, + name: json['name'] as String?, + ); + +Map _$UserToJson(User instance) => { + 'id': instance.id, + 'image_url': instance.imageUrl, + 'name': instance.name, + }; + +Review _$ReviewFromJson(Map json) => Review( + id: json['id'] as String?, + rating: (json['rating'] as num?)?.toInt(), + user: json['user'] == null + ? null + : User.fromJson(json['user'] as Map), + text: json['text'] as String?, + ); + +Map _$ReviewToJson(Review instance) => { + 'id': instance.id, + 'rating': instance.rating, + 'user': instance.user, + 'text': instance.text, + }; + +Location _$LocationFromJson(Map json) => Location( + formattedAddress: json['formatted_address'] as String?, + ); + +Map _$LocationToJson(Location instance) => { + 'formatted_address': instance.formattedAddress, + }; + +Restaurant _$RestaurantFromJson(Map json) => Restaurant( + id: json['id'] as String?, + name: json['name'] as String?, + price: json['price'] as String?, + rating: (json['rating'] as num?)?.toDouble(), + photos: + (json['photos'] as List?)?.map((e) => e as String).toList(), + categories: (json['categories'] as List?) + ?.map((e) => Category.fromJson(e as Map)) + .toList(), + hours: (json['hours'] as List?) + ?.map((e) => Hours.fromJson(e as Map)) + .toList(), + reviews: (json['reviews'] as List?) + ?.map((e) => Review.fromJson(e as Map)) + .toList(), + location: json['location'] == null + ? null + : Location.fromJson(json['location'] as Map), + ); + +Map _$RestaurantToJson(Restaurant instance) => + { + 'id': instance.id, + 'name': instance.name, + 'price': instance.price, + 'rating': instance.rating, + 'photos': instance.photos, + 'categories': instance.categories, + 'hours': instance.hours, + 'reviews': instance.reviews, + 'location': instance.location, + }; + +RestaurantQueryResult _$RestaurantQueryResultFromJson( + Map json) => + RestaurantQueryResult( + total: (json['total'] as num?)?.toInt(), + restaurants: (json['business'] as List?) + ?.map((e) => Restaurant.fromJson(e as Map)) + .toList(), + ); + +Map _$RestaurantQueryResultToJson( + RestaurantQueryResult instance) => + { + 'total': instance.total, + 'business': instance.restaurants, + }; diff --git a/lib/features/restaurant/domain/repositories/restaurant_repository.dart b/lib/features/restaurant/domain/repositories/restaurant_repository.dart new file mode 100644 index 00000000..a0a265c2 --- /dev/null +++ b/lib/features/restaurant/domain/repositories/restaurant_repository.dart @@ -0,0 +1,44 @@ +import 'package:get_it/get_it.dart'; +import 'package:restaurantour/features/restaurant/data/local_restaurant_datasource.dart'; +import 'package:restaurantour/features/restaurant/data/remote_restaurant_datasource.dart'; +import 'package:restaurantour/features/restaurant/domain/models/restaurant.dart'; + +abstract class RestaurantRepository { + Future> getRestaurants(int offset); + Future insertFavoriteRestaurants(Restaurant restaurant); + Future> getFavoriteRestaurants(); + Future deleteFavoriteRestaurant(Restaurant restaurant); +} + +class RestaurantRepositoryImpl extends RestaurantRepository { + final _remoteRestaurantDataSource = + GetIt.instance(); + + final _localRestaurantDataSource = + GetIt.instance(); + + RestaurantRepositoryImpl(); + + @override + Future> getRestaurants(int offset) async { + final response = await _remoteRestaurantDataSource.getRestaurants(offset); + final List restaurants = []; + for (final business in response.data!['data']['search']['business']) { + restaurants.add(Restaurant.fromJson(business)); + } + return restaurants; + } + + @override + Future> getFavoriteRestaurants() => + _localRestaurantDataSource.getFavoriteRestaurants(); + + @override + Future insertFavoriteRestaurants(Restaurant restaurant) => + _localRestaurantDataSource.insertFavoriteRestaurant(restaurant); + + @override + Future deleteFavoriteRestaurant(Restaurant restaurant) { + return _localRestaurantDataSource.deleteFavoriteRestaurant(restaurant); + } +} diff --git a/lib/features/restaurant/domain/use_cases/add_favorite_restaurant_use_case.dart b/lib/features/restaurant/domain/use_cases/add_favorite_restaurant_use_case.dart new file mode 100644 index 00000000..0115c963 --- /dev/null +++ b/lib/features/restaurant/domain/use_cases/add_favorite_restaurant_use_case.dart @@ -0,0 +1,13 @@ +import 'package:get_it/get_it.dart'; +import 'package:restaurantour/features/restaurant/domain/models/restaurant.dart'; +import 'package:restaurantour/features/restaurant/domain/repositories/restaurant_repository.dart'; + +class InsertFavoriteRestaurantUseCase { + final _repository = GetIt.instance(); + + InsertFavoriteRestaurantUseCase(); + + Future call(Restaurant restaurant) async { + return await _repository.insertFavoriteRestaurants(restaurant); + } +} diff --git a/lib/features/restaurant/domain/use_cases/delete_favorite_restaurant_use_case.dart b/lib/features/restaurant/domain/use_cases/delete_favorite_restaurant_use_case.dart new file mode 100644 index 00000000..cbc6ccc8 --- /dev/null +++ b/lib/features/restaurant/domain/use_cases/delete_favorite_restaurant_use_case.dart @@ -0,0 +1,14 @@ +import 'package:get_it/get_it.dart'; +import 'package:restaurantour/features/restaurant/domain/models/restaurant.dart'; +import 'package:restaurantour/features/restaurant/domain/repositories/restaurant_repository.dart'; + +class DeleteFavoriteRestaurantUseCase { + DeleteFavoriteRestaurantUseCase(); + + final RestaurantRepository repository = + GetIt.instance(); + + Future call(Restaurant restaurant) { + return repository.deleteFavoriteRestaurant(restaurant); + } +} diff --git a/lib/features/restaurant/domain/use_cases/get_favorites_restaurants_use_case.dart b/lib/features/restaurant/domain/use_cases/get_favorites_restaurants_use_case.dart new file mode 100644 index 00000000..e0b11dbb --- /dev/null +++ b/lib/features/restaurant/domain/use_cases/get_favorites_restaurants_use_case.dart @@ -0,0 +1,13 @@ +import 'package:get_it/get_it.dart'; +import 'package:restaurantour/features/restaurant/domain/models/restaurant.dart'; +import 'package:restaurantour/features/restaurant/domain/repositories/restaurant_repository.dart'; + +class GetFavoriteRestaurantsUseCase { + final _repository = GetIt.instance(); + + GetFavoriteRestaurantsUseCase(); + + Future> call() async { + return await _repository.getFavoriteRestaurants(); + } +} diff --git a/lib/features/restaurant/domain/use_cases/get_restaurants_use_case.dart b/lib/features/restaurant/domain/use_cases/get_restaurants_use_case.dart new file mode 100644 index 00000000..0b08dda3 --- /dev/null +++ b/lib/features/restaurant/domain/use_cases/get_restaurants_use_case.dart @@ -0,0 +1,13 @@ +import 'package:get_it/get_it.dart'; +import 'package:restaurantour/features/restaurant/domain/models/restaurant.dart'; +import 'package:restaurantour/features/restaurant/domain/repositories/restaurant_repository.dart'; + +class GetRestaurantsUseCase { + final _repository = GetIt.instance(); + + GetRestaurantsUseCase(); + + Future> call(int offset) async { + return await _repository.getRestaurants(offset); + } +} diff --git a/lib/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_cubit.dart b/lib/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_cubit.dart new file mode 100644 index 00000000..b470c590 --- /dev/null +++ b/lib/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_cubit.dart @@ -0,0 +1,49 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:restaurantour/features/restaurant/domain/models/restaurant.dart'; +import 'package:restaurantour/features/restaurant/domain/use_cases/add_favorite_restaurant_use_case.dart'; +import 'package:restaurantour/features/restaurant/domain/use_cases/delete_favorite_restaurant_use_case.dart'; +import 'package:restaurantour/features/restaurant/domain/use_cases/get_favorites_restaurants_use_case.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_state.dart'; + +class RestaurantDetailCubit extends Cubit { + RestaurantDetailCubit() : super(RestaurantDetailLoading()); + + final getFavoriteRestaurantsUseCase = + GetIt.instance(); + final deleteFavoriteRestaurantsUseCase = + GetIt.instance(); + final insertFavoriteRestaurantsUseCase = + GetIt.instance(); + + void fetchRestaurantDetail(Restaurant restaurant) async { + final favoritedRestaurantsList = await getFavoriteRestaurantsUseCase(); + emit( + RestaurantDetailLoaded( + isFavorited: favoritedRestaurantsList.contains(restaurant), + restaurant: restaurant, + ), + ); + } + + void toggleFavorite() { + final currentState = state; + if (currentState is RestaurantDetailLoaded) { + if (currentState.isFavorited) { + deleteFavoriteRestaurantsUseCase(currentState.restaurant); + emit( + currentState.copyWith( + isFavorited: false, + ), + ); + } else { + insertFavoriteRestaurantsUseCase(currentState.restaurant); + emit( + currentState.copyWith( + isFavorited: true, + ), + ); + } + } + } +} diff --git a/lib/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_state.dart b/lib/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_state.dart new file mode 100644 index 00000000..736f98c3 --- /dev/null +++ b/lib/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_state.dart @@ -0,0 +1,33 @@ +import 'package:equatable/equatable.dart'; +import 'package:restaurantour/features/restaurant/domain/models/restaurant.dart'; + +abstract class RestaurantDetailState extends Equatable { + const RestaurantDetailState(); + + @override + List get props => []; +} + +class RestaurantDetailLoading extends RestaurantDetailState {} + +class RestaurantDetailLoaded extends RestaurantDetailState { + const RestaurantDetailLoaded({ + required this.isFavorited, + required this.restaurant, + }); + final bool isFavorited; + final Restaurant restaurant; + + @override + List get props => [isFavorited, restaurant]; + + RestaurantDetailLoaded copyWith({ + bool? isFavorited, + Restaurant? restaurant, + }) { + return RestaurantDetailLoaded( + isFavorited: isFavorited ?? this.isFavorited, + restaurant: restaurant ?? this.restaurant, + ); + } +} diff --git a/lib/features/restaurant/presentation/restaurant_details/restaurant_details_page.dart b/lib/features/restaurant/presentation/restaurant_details/restaurant_details_page.dart new file mode 100644 index 00000000..1840c269 --- /dev/null +++ b/lib/features/restaurant/presentation/restaurant_details/restaurant_details_page.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:restaurantour/core/design_system/restaurantour_design_system.dart'; +import 'package:restaurantour/core/design_system/widgets/open_status_widget.dart'; +import 'package:restaurantour/core/design_system/widgets/rating_widget.dart'; +import 'package:restaurantour/features/restaurant/domain/models/restaurant.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_state.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_cubit.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class RestaurantDetailsPage extends StatelessWidget { + const RestaurantDetailsPage({Key? key}) : super(key: key); + + static const routeName = '/restaurant_detail'; + + @override + Widget build(BuildContext context) { + final Restaurant restaurant = + ModalRoute.of(context)!.settings.arguments as Restaurant; + + return Scaffold( + appBar: AppBar( + title: Text( + restaurant.name ?? 'Restaurant Name', + style: RestaurantourTextStyles.heading6, + ), + actions: [ + BlocProvider( + create: (context) => GetIt.instance() + ..fetchRestaurantDetail(restaurant), + child: BlocBuilder( + builder: (context, state) { + if (state is RestaurantDetailLoaded) { + return IconButton( + icon: Icon( + state.isFavorited + ? Icons.favorite + : Icons.favorite_border, + ), + onPressed: () { + context.read().toggleFavorite(); + }, + ); + } + + return const CircularProgressIndicator(); + }, + ), + ), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + Hero( + tag: restaurant.id ?? 'restaurant.heroImage', + child: Image.network( + restaurant.heroImage, + height: 400, + width: double.infinity, + fit: BoxFit.cover, + ), + ), + Padding( + padding: const EdgeInsets.all(RestaurantourSizes.size5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: RestaurantourSizes.size3), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${restaurant.price ?? ''} ${restaurant.displayCategory}', + style: RestaurantourTextStyles.caption, + ), + OpenStatus(isOpen: restaurant.isOpen), + ], + ), + const SizedBox(height: RestaurantourSizes.size6), + const Divider(), + const SizedBox(height: RestaurantourSizes.size6), + Text( + AppLocalizations.of(context)!.address, + style: RestaurantourTextStyles.caption, + ), + const SizedBox(height: RestaurantourSizes.size6), + Text( + restaurant.location?.formattedAddress ?? '', + style: RestaurantourTextStyles.caption + .copyWith(fontWeight: FontWeight.w600, fontSize: 14), + ), + const SizedBox(height: RestaurantourSizes.size5), + const Divider(), + const SizedBox(height: RestaurantourSizes.size5), + Text( + AppLocalizations.of(context)!.overallRating, + style: RestaurantourTextStyles.caption, + ), + const SizedBox(height: RestaurantourSizes.size5), + Row( + children: [ + Text( + '${restaurant.rating ?? 0}', + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.w700, + fontFamily: 'Lora', + ), + ), + const SizedBox(width: RestaurantourSizes.size2), + const Icon( + Icons.star, + color: Colors.amber, + size: RestaurantourSizes.size5, + ), + ], + ), + const SizedBox(height: RestaurantourSizes.size5), + const Divider(), + Text( + '${restaurant.reviews?.length ?? 0} ${AppLocalizations.of(context)!.reviews}', + style: RestaurantourTextStyles.caption, + ), + const SizedBox(height: RestaurantourSizes.size5), + ListView.builder( + itemCount: restaurant.reviews?.length ?? 0, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final review = restaurant.reviews![index]; + return Padding( + padding: const EdgeInsets.only( + bottom: RestaurantourSizes.size5, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RatingWidget(rating: restaurant.rating ?? 0), + const SizedBox(height: RestaurantourSizes.size2), + Text( + review.text ?? 'Review Text', + maxLines: review.rating, + overflow: TextOverflow.ellipsis, + style: RestaurantourTextStyles.body1, + ), + const SizedBox(height: RestaurantourSizes.size3), + Row( + children: [ + CircleAvatar( + backgroundImage: NetworkImage( + review.user?.imageUrl ?? '', + ), + radius: 20, + ), + const SizedBox(width: 8), + Text( + review.user?.name ?? 'User Name', + style: RestaurantourTextStyles.caption, + ), + ], + ), + ], + ), + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/restaurant/presentation/restaurants_list/bloc/restaurant_list_cubit.dart b/lib/features/restaurant/presentation/restaurants_list/bloc/restaurant_list_cubit.dart new file mode 100644 index 00000000..e9f57533 --- /dev/null +++ b/lib/features/restaurant/presentation/restaurants_list/bloc/restaurant_list_cubit.dart @@ -0,0 +1,36 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:restaurantour/features/restaurant/domain/models/restaurant.dart'; +import 'package:restaurantour/features/restaurant/domain/use_cases/get_favorites_restaurants_use_case.dart'; +import 'package:restaurantour/features/restaurant/domain/use_cases/get_restaurants_use_case.dart'; + +part 'restaurant_list_state.dart'; + +class RestaurantListCubit extends Cubit { + RestaurantListCubit() : super(RestaurantListLoading()); + + final getRestaurantsUseCase = GetIt.instance(); + final getFavoriteRestaurantsUseCase = + GetIt.instance(); + + void fetchRestaurants() async { + emit(RestaurantListLoading()); + try { + final restaurants = await getRestaurantsUseCase.call(0); + emit(RestaurantListLoaded(restaurants)); + } catch (e) { + emit(RestaurantListError(e.toString())); + } + } + + void fetchFavoriteRestaurants() async { + emit(RestaurantListLoading()); + try { + final favoriteRestaurants = await getFavoriteRestaurantsUseCase.call(); + emit(FavoriteRestaurantsLoaded(favoriteRestaurants)); + } catch (e) { + emit(RestaurantListError(e.toString())); + } + } +} diff --git a/lib/features/restaurant/presentation/restaurants_list/bloc/restaurant_list_state.dart b/lib/features/restaurant/presentation/restaurants_list/bloc/restaurant_list_state.dart new file mode 100644 index 00000000..d5f51473 --- /dev/null +++ b/lib/features/restaurant/presentation/restaurants_list/bloc/restaurant_list_state.dart @@ -0,0 +1,37 @@ +part of 'restaurant_list_cubit.dart'; + +abstract class RestaurantListState extends Equatable { + const RestaurantListState(); + + @override + List get props => []; +} + +class RestaurantListLoading extends RestaurantListState {} + +class RestaurantListLoaded extends RestaurantListState { + final List restaurants; + + const RestaurantListLoaded(this.restaurants); + + @override + List get props => [restaurants]; +} + +class FavoriteRestaurantsLoaded extends RestaurantListState { + final List restaurants; + + const FavoriteRestaurantsLoaded(this.restaurants); + + @override + List get props => [restaurants]; +} + +class RestaurantListError extends RestaurantListState { + final String message; + + const RestaurantListError(this.message); + + @override + List get props => [message]; +} diff --git a/lib/features/restaurant/presentation/restaurants_list/favorite_restaurant_list.dart b/lib/features/restaurant/presentation/restaurants_list/favorite_restaurant_list.dart new file mode 100644 index 00000000..53881a4a --- /dev/null +++ b/lib/features/restaurant/presentation/restaurants_list/favorite_restaurant_list.dart @@ -0,0 +1,50 @@ +part of 'restaurant_list_page.dart'; + +class _FavoriteRestaurantList extends StatelessWidget { + const _FavoriteRestaurantList({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: GetIt.instance(), + child: BlocBuilder( + builder: (context, state) { + if (state is RestaurantListLoading) { + return const Center( + child: CircularProgressIndicator(), + ); + } + + if (state is FavoriteRestaurantsLoaded) { + return ListView.builder( + itemCount: state.restaurants.length, + itemBuilder: (context, index) { + final restaurant = state.restaurants[index]; + return RestaurantItemWidget( + restaurant: restaurant, + ); + }, + ); + } + + if (state is RestaurantListError) { + return Center( + child: Text( + state.message, + style: RestaurantourTextStyles.caption.copyWith( + color: Colors.red, + ), + ), + ); + } + + return const Center( + child: Text('Restaurantour'), + ); + }, + ), + ); + } +} diff --git a/lib/features/restaurant/presentation/restaurants_list/restaurant_list.dart b/lib/features/restaurant/presentation/restaurants_list/restaurant_list.dart new file mode 100644 index 00000000..5bf0af41 --- /dev/null +++ b/lib/features/restaurant/presentation/restaurants_list/restaurant_list.dart @@ -0,0 +1,65 @@ +part of 'restaurant_list_page.dart'; + +class _RestaurantList extends StatelessWidget { + const _RestaurantList({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: GetIt.instance(), + child: BlocBuilder( + builder: (context, state) { + if (state is RestaurantListLoading) { + return ListView.builder( + itemCount: 5, + itemBuilder: (context, index) { + return const ShimmerRestaurantItemWidget(); + }, + ); + } + + if (state is RestaurantListLoaded) { + return ListView.builder( + itemCount: state.restaurants.length, + itemBuilder: (context, index) { + final restaurant = state.restaurants[index]; + return RestaurantItemWidget( + restaurant: restaurant, + ); + }, + ); + } + + if (state is FavoriteRestaurantsLoaded) { + return ListView.builder( + itemCount: state.restaurants.length, + itemBuilder: (context, index) { + final restaurant = state.restaurants[index]; + return RestaurantItemWidget( + restaurant: restaurant, + ); + }, + ); + } + + if (state is RestaurantListError) { + return Center( + child: Text( + state.message, + style: RestaurantourTextStyles.caption.copyWith( + color: Colors.red, + ), + ), + ); + } + + return const Center( + child: Text('Restaurantour'), + ); + }, + ), + ); + } +} diff --git a/lib/features/restaurant/presentation/restaurants_list/restaurant_list_page.dart b/lib/features/restaurant/presentation/restaurants_list/restaurant_list_page.dart new file mode 100644 index 00000000..f66463a8 --- /dev/null +++ b/lib/features/restaurant/presentation/restaurants_list/restaurant_list_page.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:restaurantour/core/design_system/restaurantour_design_system.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurants_list/bloc/restaurant_list_cubit.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurants_list/widgets/restaurant_item_shimmer_widget.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurants_list/widgets/restaurant_item_widget.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +part 'restaurant_list.dart'; +part 'favorite_restaurant_list.dart'; + +class RestaurantListPage extends StatefulWidget { + const RestaurantListPage({Key? key}) : super(key: key); + + @override + _RestaurantListPageState createState() => _RestaurantListPageState(); +} + +class _RestaurantListPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final cubit = GetIt.instance(); + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + cubit.fetchRestaurants(); + _tabController.addListener(() { + if (_tabController.indexIsChanging) { + if (_tabController.index == 0) { + cubit.fetchRestaurants(); + } else { + cubit.fetchFavoriteRestaurants(); + } + } + }); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + AppLocalizations.of(context)!.appTitle, + style: RestaurantourTextStyles.heading6, + ), + centerTitle: true, + bottom: PreferredSize( + preferredSize: const Size(0, RestaurantourSizes.size8), + child: Container( + alignment: Alignment.center, + child: TabBar( + tabAlignment: TabAlignment.center, + labelColor: Colors.black, + isScrollable: true, + controller: _tabController, + indicatorColor: Colors.black, + indicatorSize: TabBarIndicatorSize.label, + unselectedLabelColor: Colors.grey, + indicator: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: Colors.black, + width: RestaurantourSizes.size1, + ), + ), + ), + labelStyle: const TextStyle( + fontWeight: FontWeight.w600, + fontSize: RestaurantourSizes.size5, + ), + tabs: [ + Tab( + text: AppLocalizations.of(context)!.allRestaurants, + ), + Tab(text: AppLocalizations.of(context)!.myFavorites), + ], + ), + ), + ), + ), + body: Column( + children: [ + Expanded( + child: TabBarView( + controller: _tabController, + children: const [ + _RestaurantList(), + _FavoriteRestaurantList(), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/restaurant/presentation/restaurants_list/widgets/restaurant_item_shimmer_widget.dart b/lib/features/restaurant/presentation/restaurants_list/widgets/restaurant_item_shimmer_widget.dart new file mode 100644 index 00000000..13966bf7 --- /dev/null +++ b/lib/features/restaurant/presentation/restaurants_list/widgets/restaurant_item_shimmer_widget.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/core/design_system/restaurantour_design_system.dart'; +import 'package:shimmer/shimmer.dart'; + +class ShimmerRestaurantItemWidget extends StatelessWidget { + const ShimmerRestaurantItemWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Shimmer.fromColors( + baseColor: Colors.grey[300]!, + highlightColor: Colors.grey[100]!, + child: Container( + margin: const EdgeInsets.all(RestaurantourSizes.size3), + padding: const EdgeInsets.all(RestaurantourSizes.size5), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[300]!), + borderRadius: BorderRadius.circular(RestaurantourSizes.size3), + ), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(RestaurantourSizes.size3), + child: Container( + width: RestaurantourSizes.size10, + height: RestaurantourSizes.size10, + color: Colors.white, + ), + ), + Expanded( + child: Container( + margin: const EdgeInsets.only(left: RestaurantourSizes.size6), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(RestaurantourSizes.size2), + color: Colors.white, + ), + width: double.infinity, + height: RestaurantourSizes.size6, + ), + const SizedBox(height: RestaurantourSizes.size2), + Container( + width: MediaQuery.of(context).size.width * 0.6, + height: RestaurantourSizes.size6, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(RestaurantourSizes.size2), + color: Colors.white, + ), + ), + const SizedBox(height: RestaurantourSizes.size2), + Container( + width: RestaurantourSizes.size5, + height: RestaurantourSizes.size4, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(RestaurantourSizes.size2), + color: Colors.white, + ), + ), + const SizedBox(height: RestaurantourSizes.size2), + Container( + width: RestaurantourSizes.size7, + height: RestaurantourSizes.size3, + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(RestaurantourSizes.size2), + color: Colors.white, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/restaurant/presentation/restaurants_list/widgets/restaurant_item_widget.dart b/lib/features/restaurant/presentation/restaurants_list/widgets/restaurant_item_widget.dart new file mode 100644 index 00000000..d1a42f2f --- /dev/null +++ b/lib/features/restaurant/presentation/restaurants_list/widgets/restaurant_item_widget.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/core/design_system/restaurantour_design_system.dart'; +import 'package:restaurantour/core/design_system/widgets/open_status_widget.dart'; +import 'package:restaurantour/core/design_system/widgets/rating_widget.dart'; +import 'package:restaurantour/features/restaurant/domain/models/restaurant.dart'; + +class RestaurantItemWidget extends StatelessWidget { + const RestaurantItemWidget({required this.restaurant, Key? key}) + : super(key: key); + + final Restaurant restaurant; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.of(context).pushNamed( + '/restaurant_detail', + arguments: restaurant, + ); + }, + child: Container( + margin: const EdgeInsets.all(RestaurantourSizes.size3), + padding: const EdgeInsets.all(RestaurantourSizes.size5), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[300]!), + borderRadius: BorderRadius.circular(8), + ), + height: RestaurantourSizes.size11, + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(RestaurantourSizes.size3), + child: Hero( + tag: restaurant.id ?? 'restaurant.heroImage', + child: Image.network( + restaurant.heroImage, + width: 88, + height: 88, + fit: BoxFit.cover, + ), + ), + ), + const SizedBox( + width: RestaurantourSizes.size4, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + restaurant.name ?? '', + style: RestaurantourTextStyles.subtitle1, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + Text( + '${restaurant.price ?? ''} ${restaurant.displayCategory}', + style: RestaurantourTextStyles.caption, + ), + Align( + alignment: Alignment.bottomLeft, + child: Row( + children: [ + RatingWidget(rating: restaurant.rating ?? 0), + Expanded(child: Container()), + OpenStatus(isOpen: restaurant.isOpen), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/restaurant/restaurant.dart b/lib/features/restaurant/restaurant.dart new file mode 100644 index 00000000..685001a3 --- /dev/null +++ b/lib/features/restaurant/restaurant.dart @@ -0,0 +1,68 @@ +import 'package:get_it/get_it.dart'; +import 'package:hive/hive.dart'; +import 'package:restaurantour/features/restaurant/data/local_restaurant_datasource.dart'; +import 'package:restaurantour/features/restaurant/data/remote_restaurant_datasource.dart'; +import 'package:restaurantour/features/restaurant/domain/models/restaurant.dart'; +import 'package:restaurantour/features/restaurant/domain/repositories/restaurant_repository.dart'; +import 'package:restaurantour/features/restaurant/domain/use_cases/add_favorite_restaurant_use_case.dart'; +import 'package:restaurantour/features/restaurant/domain/use_cases/delete_favorite_restaurant_use_case.dart'; +import 'package:restaurantour/features/restaurant/domain/use_cases/get_favorites_restaurants_use_case.dart'; +import 'package:restaurantour/features/restaurant/domain/use_cases/get_restaurants_use_case.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_cubit.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurants_list/bloc/restaurant_list_cubit.dart'; + +export 'package:restaurantour/features/restaurant/presentation/restaurant_details/restaurant_details_page.dart'; +export 'package:restaurantour/features/restaurant/presentation/restaurants_list/restaurant_list_page.dart'; + +final _getIt = GetIt.instance; + +void initRestaurantDependecies() { + _initHive(); + _initDataSources(); + _initRepositories(); + _initUseCases(); + _initBlocs(); +} + +void _initHive() { + Hive + ..registerAdapter(CategoryAdapter()) + ..registerAdapter(HoursAdapter()) + ..registerAdapter(UserAdapter()) + ..registerAdapter(ReviewAdapter()) + ..registerAdapter(LocationAdapter()) + ..registerAdapter(RestaurantAdapter()); + Hive.openBox('favorite_restaurants'); +} + +void _initDataSources() { + _getIt.registerFactory( + () => RemoteRestaurantDataSourceImpl(), + ); + _getIt.registerFactory( + () => LocalRestaurantDataSourceImpl(), + ); +} + +void _initRepositories() { + _getIt + .registerFactory(() => RestaurantRepositoryImpl()); +} + +void _initUseCases() { + _getIt.registerFactory(() => GetRestaurantsUseCase()); + _getIt.registerFactory( + () => GetFavoriteRestaurantsUseCase(), + ); + _getIt.registerFactory( + () => InsertFavoriteRestaurantUseCase(), + ); + _getIt.registerFactory( + () => DeleteFavoriteRestaurantUseCase(), + ); +} + +void _initBlocs() { + _getIt.registerSingleton(RestaurantListCubit()); + _getIt.registerFactory(() => RestaurantDetailCubit()); +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 00000000..f26e208b --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,35 @@ +{ + "@@locale": "en", + "appTitle": "Restaurantour", + "@appTitle": { + "description": "The title of the app" + }, + "allRestaurants": "All Restaurants", + "@allRestaurants": { + "description": "The title of the page that lists all restaurants" + }, + "myFavorites": "My Favorites", + "@myFavorites": { + "description": "The title of the page that lists of favorite restaurants" + }, + "openNow": "Open Now", + "@openNow": { + "description": "The title of the page that lists of restaurants that are currently open" + }, + "closed": "Closed", + "@closed": { + "description": "The title of the page that lists of restaurants that are currently closed" + }, + "address": "Address", + "@address": { + "description": "The label for the address of a restaurant" + }, + "overallRating": "Overall Rating", + "@overallRating": { + "description": "The label for the overall rating of a restaurant" + }, + "reviews": "Reviews", + "@reviews": { + "description": "The label for the reviews of a restaurant" + } +} \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb new file mode 100644 index 00000000..92e15fbf --- /dev/null +++ b/lib/l10n/app_es.arb @@ -0,0 +1,11 @@ +{ + "@@locale": "es", + "appTitle": "Restaurantour", + "allRestaurants": "Todos los restaurantes", + "myFavorites": "Mis favoritos", + "openNow": "Abierto ahora", + "closed": "Cerrado", + "address": "Dirección", + "overallRating": "Calificación general", + "reviews": "Reseñas" +} \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb new file mode 100644 index 00000000..f05dbe74 --- /dev/null +++ b/lib/l10n/app_pt.arb @@ -0,0 +1,11 @@ +{ + "@@locale": "pt", + "appTitle": "Restaurantour", + "allRestaurants": "Todos os restaurantes", + "myFavorites": "Meus favoritos", + "openNow": "Aberto agora", + "closed": "Fechado", + "address": "Endereço", + "overallRating": "Classificação geral", + "reviews": "Avaliações" +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index c6ce7473..4106a554 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,57 +1,44 @@ import 'package:flutter/material.dart'; -import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:restaurantour/core/dependecy_injection.dart'; +import 'package:restaurantour/core/hive/hive.dart'; +import 'package:restaurantour/features/restaurant/restaurant.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await initHive(); + initDependencies(); + initRestaurantDependecies(); 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', + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en'), + Locale('es'), + Locale('pt'), + ], 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 RestaurantListPage(), + routes: { + RestaurantDetailsPage.routeName: (context) => + const RestaurantDetailsPage(), + }, ); } } diff --git a/lib/models/restaurant.g.dart b/lib/models/restaurant.g.dart deleted file mode 100644 index 3ed33f9a..00000000 --- a/lib/models/restaurant.g.dart +++ /dev/null @@ -1,109 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'restaurant.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Category _$CategoryFromJson(Map json) => Category( - alias: json['alias'] as String?, - title: json['title'] as String?, - ); - -Map _$CategoryToJson(Category instance) => { - 'alias': instance.alias, - 'title': instance.title, - }; - -Hours _$HoursFromJson(Map json) => Hours( - isOpenNow: json['is_open_now'] as bool?, - ); - -Map _$HoursToJson(Hours instance) => { - 'is_open_now': instance.isOpenNow, - }; - -User _$UserFromJson(Map json) => User( - id: json['id'] as String?, - imageUrl: json['image_url'] as String?, - name: json['name'] as String?, - ); - -Map _$UserToJson(User instance) => { - 'id': instance.id, - 'image_url': instance.imageUrl, - 'name': instance.name, - }; - -Review _$ReviewFromJson(Map json) => Review( - id: json['id'] as String?, - rating: json['rating'] as int?, - user: json['user'] == null - ? null - : User.fromJson(json['user'] as Map), - ); - -Map _$ReviewToJson(Review instance) => { - 'id': instance.id, - 'rating': instance.rating, - 'user': instance.user, - }; - -Location _$LocationFromJson(Map json) => Location( - formattedAddress: json['formatted_address'] as String?, - ); - -Map _$LocationToJson(Location instance) => { - 'formatted_address': instance.formattedAddress, - }; - -Restaurant _$RestaurantFromJson(Map json) => Restaurant( - id: json['id'] as String?, - name: json['name'] as String?, - price: json['price'] as String?, - rating: (json['rating'] as num?)?.toDouble(), - photos: - (json['photos'] as List?)?.map((e) => e as String).toList(), - categories: (json['categories'] as List?) - ?.map((e) => Category.fromJson(e as Map)) - .toList(), - hours: (json['hours'] as List?) - ?.map((e) => Hours.fromJson(e as Map)) - .toList(), - reviews: (json['reviews'] as List?) - ?.map((e) => Review.fromJson(e as Map)) - .toList(), - location: json['location'] == null - ? null - : Location.fromJson(json['location'] as Map), - ); - -Map _$RestaurantToJson(Restaurant instance) => - { - 'id': instance.id, - 'name': instance.name, - 'price': instance.price, - 'rating': instance.rating, - 'photos': instance.photos, - 'categories': instance.categories, - 'hours': instance.hours, - 'reviews': instance.reviews, - 'location': instance.location, - }; - -RestaurantQueryResult _$RestaurantQueryResultFromJson( - Map json) => - RestaurantQueryResult( - total: json['total'] as int?, - restaurants: (json['business'] as List?) - ?.map((e) => Restaurant.fromJson(e as Map)) - .toList(), - ); - -Map _$RestaurantQueryResultToJson( - RestaurantQueryResult instance) => - { - 'total': instance.total, - 'business': instance.restaurants, - }; 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..93ee6a69 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 dev" + 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: @@ -157,26 +165,34 @@ packages: dependency: transitive description: name: convert - sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "1.8.0" 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: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dart_style: dependency: transitive 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: @@ -230,14 +278,19 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_svg: 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 @@ -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,54 +328,110 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.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.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "0.18.1" 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: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + url: "https://pub.dev" + source: hosted + version: "6.8.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 url: "https://pub.dev" source: hosted - version: "6.7.1" + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -327,58 +444,98 @@ packages: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" matcher: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: name: mime - sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" + mockito: + dependency: transitive + description: + name: mockito + sha256: "6841eed20a7befac0ce07df8116c8b8233ed1f4486a7647c7fc5a02ae6163917" + url: "https://pub.dev" + source: hosted + version: "5.4.4" + mocktail: + dependency: "direct dev" + 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" + network_image_mock: + dependency: "direct dev" + description: + name: network_image_mock + sha256: "855cdd01d42440e0cffee0d6c2370909fc31b3bcba308a59829f24f64be42db7" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + 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: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -387,54 +544,150 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider: + dependency: "direct main" + 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: "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: "6.0.2" + 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: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" shelf: dependency: transitive description: name: shelf - sha256: c240984c924796e055e831a0a36db23be8cb04f170b26df572931ab36418421d + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" + shelf_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" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter @@ -456,6 +709,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: @@ -484,10 +753,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -504,6 +773,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f + url: "https://pub.dev" + source: hosted + version: "1.24.9" test_api: dependency: transitive description: @@ -512,46 +789,54 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + test_core: + dependency: transitive + description: + name: test_core + sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a + url: "https://pub.dev" + source: hosted + version: "0.5.9" 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 +845,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - watcher: + vm_service: dependency: transitive description: - name: watcher - sha256: e42dfcc48f67618344da967b10f62de57e04bae01d9d3af4c2596f3712a88c99 + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "1.0.1" - web: + version: "13.0.0" + watcher: dependency: transitive description: - name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "1.1.0" 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.2.0-0 <4.0.0" + flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index be3055e0..dbdc1792 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,10 @@ 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" @@ -14,8 +13,17 @@ dependencies: sdk: flutter cupertino_icons: ^1.0.6 dio: ^5.4.0 - json_annotation: ^4.8.1 + json_annotation: ^4.9.0 flutter_svg: ^2.0.9 + get_it: ^7.6.7 + flutter_bloc: ^8.1.5 + equatable: ^2.0.5 + hive: ^2.2.3 + path_provider: ^2.1.3 + flutter_localizations: + sdk: flutter + intl: ^0.18.1 + shimmer: ^3.0.0 dev_dependencies: flutter_test: @@ -23,8 +31,19 @@ dev_dependencies: flutter_lints: ^1.0.2 build_runner: ^2.4.8 json_serializable: ^6.7.1 + hive_generator: ^2.0.1 + mocktail: ^1.0.3 + network_image_mock: ^2.1.1 + bloc_test: ^9.1.7 flutter: + generate: true uses-material-design: true -# assets: -# - assets/svg/ \ No newline at end of file + fonts: + - family: Lora + fonts: + - asset: assets/fonts/Lora-Regular.ttf + - asset: assets/fonts/Lora-Medium.ttf + weight: 500 + - asset: assets/fonts/Lora-Bold.ttf + weight: 700 diff --git a/test/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_cubit_test.dart b/test/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_cubit_test.dart new file mode 100644 index 00000000..f64d12b9 --- /dev/null +++ b/test/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_cubit_test.dart @@ -0,0 +1,86 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurantour/features/restaurant/domain/use_cases/add_favorite_restaurant_use_case.dart'; +import 'package:restaurantour/features/restaurant/domain/use_cases/delete_favorite_restaurant_use_case.dart'; +import 'package:restaurantour/features/restaurant/domain/use_cases/get_favorites_restaurants_use_case.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_state.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_cubit.dart'; + +import '../../../restaurant_mockup.dart'; + +class MockGetFavoriteRestaurantsUseCase extends Mock + implements GetFavoriteRestaurantsUseCase {} + +class MockDeleteFavoriteRestaurantUseCase extends Mock + implements DeleteFavoriteRestaurantUseCase {} + +class MockInsertFavoriteRestaurantUseCase extends Mock + implements InsertFavoriteRestaurantUseCase {} + +void main() { + late RestaurantDetailCubit cubit; + late MockGetFavoriteRestaurantsUseCase mockGetFavoriteRestaurantsUseCase; + late MockDeleteFavoriteRestaurantUseCase mockDeleteFavoriteRestaurantUseCase; + late MockInsertFavoriteRestaurantUseCase mockInsertFavoriteRestaurantUseCase; + + setUp(() { + mockGetFavoriteRestaurantsUseCase = MockGetFavoriteRestaurantsUseCase(); + mockDeleteFavoriteRestaurantUseCase = MockDeleteFavoriteRestaurantUseCase(); + mockInsertFavoriteRestaurantUseCase = MockInsertFavoriteRestaurantUseCase(); + GetIt.I.registerSingleton( + mockGetFavoriteRestaurantsUseCase, + ); + GetIt.I.registerSingleton( + mockDeleteFavoriteRestaurantUseCase, + ); + GetIt.I.registerSingleton( + mockInsertFavoriteRestaurantUseCase, + ); + cubit = RestaurantDetailCubit(); + }); + + tearDown(() { + GetIt.I.unregister(); + GetIt.I.unregister(); + GetIt.I.unregister(); + }); + + blocTest( + 'emits [RestaurantDetailLoading, RestaurantDetailLoaded] when fetchRestaurantDetail is called', + build: () => cubit, + act: (cubit) { + when(() => mockGetFavoriteRestaurantsUseCase.call()) + .thenAnswer((_) async => [restaurantMockup]); + cubit.fetchRestaurantDetail( + restaurantMockup, + ); + }, + expect: () => [ + RestaurantDetailLoaded( + isFavorited: true, + restaurant: restaurantMockup, + ), + ], + ); + + blocTest( + 'emits [RestaurantDetailLoading, RestaurantDetailLoaded] when fetchRestaurantDetail is called' + 'and restaurant is not favorited', + build: () => cubit, + act: (cubit) { + when(() => mockGetFavoriteRestaurantsUseCase.call()) + .thenAnswer((_) async => []); + cubit.fetchRestaurantDetail( + restaurantMockup, + ); + }, + expect: () => [ + RestaurantDetailLoaded( + isFavorited: false, + restaurant: restaurantMockup, + ), + ], + ); +} diff --git a/test/features/restaurant/presentation/restaurant_details/restaurant_details_page_test.dart b/test/features/restaurant/presentation/restaurant_details/restaurant_details_page_test.dart new file mode 100644 index 00000000..2bc97b46 --- /dev/null +++ b/test/features/restaurant/presentation/restaurant_details/restaurant_details_page_test.dart @@ -0,0 +1,78 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_cubit.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurant_details/bloc/restaurant_details_state.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurant_details/restaurant_details_page.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../../restaurant_mockup.dart'; + +class MockRestaurantDetailsCubit extends MockCubit + implements RestaurantDetailCubit {} + +void main() { + late RestaurantDetailCubit mockRestaurantDetailsCubit; + + setUp(() { + mockRestaurantDetailsCubit = MockRestaurantDetailsCubit(); + + GetIt.I + .registerSingleton(mockRestaurantDetailsCubit); + }); + + tearDown(() { + GetIt.I.unregister(); + }); + + testWidgets('RestaurantDetailsPage should show restaurant details', + (WidgetTester tester) async { + mockNetworkImagesFor(() async { + final state = RestaurantDetailLoaded( + restaurant: restaurantMockup, + isFavorited: false, + ); + when(() => mockRestaurantDetailsCubit.state).thenReturn(state); + when( + () => + mockRestaurantDetailsCubit.fetchRestaurantDetail(restaurantMockup), + ).thenAnswer((_) async {}); + when(() => mockRestaurantDetailsCubit.stream).thenAnswer( + (_) => Stream.value(state), + ); + + await tester.pumpWidget( + MaterialApp( + locale: const Locale('en'), + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + initialRoute: '/', + routes: { + '/': (context) => TextButton( + onPressed: () { + Navigator.pushNamed( + context, + '/restaurantDetails', + arguments: restaurantMockup, + ); + }, + child: const Text('Go to Restaurant Details'), + ), + '/restaurantDetails': (context) => const RestaurantDetailsPage(), + }, + ), + ); + await tester.tap(find.byType(TextButton)); + await tester.pumpAndSettle(); + expect(find.text(restaurantMockup.name!), findsOneWidget); + }); + }); +} diff --git a/test/features/restaurant/presentation/restaurant_list/bloc/restaurant_list_cubit_test.dart b/test/features/restaurant/presentation/restaurant_list/bloc/restaurant_list_cubit_test.dart new file mode 100644 index 00000000..c11778a3 --- /dev/null +++ b/test/features/restaurant/presentation/restaurant_list/bloc/restaurant_list_cubit_test.dart @@ -0,0 +1,94 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:get_it/get_it.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurantour/features/restaurant/domain/use_cases/get_favorites_restaurants_use_case.dart'; +import 'package:restaurantour/features/restaurant/domain/use_cases/get_restaurants_use_case.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurants_list/bloc/restaurant_list_cubit.dart'; + +import '../../../restaurant_mockup.dart'; + +class MockGetRestaurantsUseCase extends Mock implements GetRestaurantsUseCase {} + +class MockGetFavoriteRestaurantsUseCase extends Mock + implements GetFavoriteRestaurantsUseCase {} + +void main() { + late RestaurantListCubit cubit; + late MockGetRestaurantsUseCase mockGetRestaurantsUseCase; + late MockGetFavoriteRestaurantsUseCase mockGetFavoriteRestaurantsUseCase; + + setUp(() { + mockGetRestaurantsUseCase = MockGetRestaurantsUseCase(); + mockGetFavoriteRestaurantsUseCase = MockGetFavoriteRestaurantsUseCase(); + GetIt.I.registerSingleton(mockGetRestaurantsUseCase); + GetIt.I.registerSingleton( + mockGetFavoriteRestaurantsUseCase, + ); + cubit = RestaurantListCubit(); + + when(() => mockGetFavoriteRestaurantsUseCase.call()) + .thenAnswer((_) async => [restaurantMockup]); + }); + + tearDown(() { + GetIt.I.unregister(); + GetIt.I.unregister(); + }); + + blocTest( + 'emits [RestaurantListLoading, RestaurantListLoaded] when fetchRestaurants succeeds', + build: () => cubit, + act: (cubit) { + when(() => mockGetRestaurantsUseCase.call(any())) + .thenAnswer((_) async => [restaurantMockup]); + cubit.fetchRestaurants(); + }, + expect: () => [ + RestaurantListLoading(), + RestaurantListLoaded([restaurantMockup]), + ], + ); + + blocTest( + 'emits [RestaurantListLoading, RestaurantListLoaded] with a empty list', + build: () => cubit, + act: (cubit) { + when(() => mockGetRestaurantsUseCase.call(any())) + .thenAnswer((_) async => []); + cubit.fetchRestaurants(); + }, + expect: () => [ + RestaurantListLoading(), + const RestaurantListLoaded([]), + ], + ); + + blocTest( + 'emits [RestaurantListLoading, RestaurantListError] when a error happens', + build: () => cubit, + act: (cubit) { + when(() => mockGetRestaurantsUseCase.call(any())) + .thenThrow(Exception('A non-null String must be provided')); + cubit.fetchRestaurants(); + }, + expect: () => [ + RestaurantListLoading(), + const RestaurantListError( + 'Exception: A non-null String must be provided', + ), + ], + ); + + blocTest( + 'emits [RestaurantListLoading, FavoriteRestaurantsLoaded] when fetchFavoriteRestaurants succeeds', + build: () => cubit, + act: (cubit) => cubit.fetchFavoriteRestaurants(), + expect: () => [ + RestaurantListLoading(), + FavoriteRestaurantsLoaded( + [restaurantMockup], + ), + ], + ); +} diff --git a/test/features/restaurant/presentation/restaurant_list/widgets/restaurant_item_widget_test.dart b/test/features/restaurant/presentation/restaurant_list/widgets/restaurant_item_widget_test.dart new file mode 100644 index 00000000..82d4abb3 --- /dev/null +++ b/test/features/restaurant/presentation/restaurant_list/widgets/restaurant_item_widget_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:restaurantour/features/restaurant/presentation/restaurants_list/widgets/restaurant_item_widget.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../../../restaurant_mockup.dart'; + +void main() { + testWidgets('RestaurantItemWidget should show correct data', + (WidgetTester tester) async { + mockNetworkImagesFor(() async { + await tester.pumpWidget( + MaterialApp( + locale: const Locale('en'), + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + home: Scaffold( + body: RestaurantItemWidget(restaurant: restaurantMockup), + ), + ), + ); + final nameFinder = find.text(restaurantMockup.name!); + expect(nameFinder, findsOneWidget); + }); + }); + + testWidgets('RestaurantItemWidget should navigate when tapped', + (WidgetTester tester) async { + mockNetworkImagesFor(() async { + await tester.pumpWidget( + MaterialApp( + locale: const Locale('en'), + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + home: Scaffold( + body: RestaurantItemWidget(restaurant: restaurantMockup), + ), + routes: { + '/restaurant_detail': (context) => const Text('/restaurant_detail'), + }, + ), + ); + // Act + await tester.tap(find.byType(RestaurantItemWidget)); + await tester.pumpAndSettle(); + + expect(find.text('/restaurant_detail'), findsOneWidget); + }); + }); +} diff --git a/test/features/restaurant/restaurant_mockup.dart b/test/features/restaurant/restaurant_mockup.dart new file mode 100644 index 00000000..6a89c8b7 --- /dev/null +++ b/test/features/restaurant/restaurant_mockup.dart @@ -0,0 +1,27 @@ +import 'package:restaurantour/features/restaurant/domain/models/restaurant.dart'; + +Restaurant restaurantMockup = Restaurant( + id: '1', + name: 'Gordon Ramsay Hell\'s Kitchen', + price: '\$\$\$', + rating: 4.5, + photos: [ + 'https://s3-media2.fl.yelpcdn.com/bphoto/q771KjLzI5y638leJsnJnQ/o.jpg', + ], + categories: [ + Category(alias: 'newamerican', title: 'New American'), + ], + hours: [const Hours(isOpenNow: true)], + reviews: [ + const Review( + id: '1', + rating: 5, + user: User( + id: '1', + imageUrl: 'https://s3-media2.fl.yelpcdn.com/photo/1.jpg', + name: 'John Doe', + ), + ), + ], + location: Location(formattedAddress: '123 Example St, City'), +); 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); - }); -}