diff --git a/.fvm/release b/.fvm/release new file mode 100644 index 00000000..4fd0f831 --- /dev/null +++ b/.fvm/release @@ -0,0 +1 @@ +3.13.9 \ No newline at end of file diff --git a/.fvm/version b/.fvm/version new file mode 100644 index 00000000..4fd0f831 --- /dev/null +++ b/.fvm/version @@ -0,0 +1 @@ +3.13.9 \ No newline at end of file diff --git a/.fvm/versions/3.13.9 b/.fvm/versions/3.13.9 new file mode 120000 index 00000000..32eeb120 --- /dev/null +++ b/.fvm/versions/3.13.9 @@ -0,0 +1 @@ +/Users/viniciusoliveira/fvm/versions/3.13.9 \ No newline at end of file diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 00000000..6108f14a --- /dev/null +++ b/.fvmrc @@ -0,0 +1,4 @@ +{ + "flutter": "3.13.9", + "flavors": {} +} \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index e47cb81d..cc7c839c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -8,7 +8,7 @@ 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.") + throw new Exception("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') @@ -57,6 +57,7 @@ android { signingConfig signingConfigs.debug } } + namespace 'com.example.restaurantour' } flutter { diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 53df105c..f880684a 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6cb1ae9c..804dde3a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + + diff --git a/android/build.gradle b/android/build.gradle index 24047dce..991bc023 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.21' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:8.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/android/gradle.properties b/android/gradle.properties index 94adc3a3..b9a9a246 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,6 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index bc6a58af..d07c7fcf 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip diff --git a/assets/fonts/Lora/Lora-Italic-VariableFont_wght.ttf b/assets/fonts/Lora/Lora-Italic-VariableFont_wght.ttf new file mode 100644 index 00000000..05cbde29 Binary files /dev/null and b/assets/fonts/Lora/Lora-Italic-VariableFont_wght.ttf differ diff --git a/assets/fonts/Lora/Lora-VariableFont_wght.ttf b/assets/fonts/Lora/Lora-VariableFont_wght.ttf new file mode 100644 index 00000000..b23ea948 Binary files /dev/null and b/assets/fonts/Lora/Lora-VariableFont_wght.ttf differ diff --git a/assets/fonts/Lora/OFL.txt b/assets/fonts/Lora/OFL.txt new file mode 100644 index 00000000..3f0fcd61 --- /dev/null +++ b/assets/fonts/Lora/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Lora Project Authors (https://github.com/cyrealtype/Lora-Cyrillic), with Reserved Font Name "Lora". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/Lora/README.txt b/assets/fonts/Lora/README.txt new file mode 100644 index 00000000..c5dd0ebd --- /dev/null +++ b/assets/fonts/Lora/README.txt @@ -0,0 +1,71 @@ +Lora Variable Font +================== + +This download contains Lora as both variable fonts and static fonts. + +Lora is a variable font with this axis: + wght + +This means all the styles are contained in these files: + Lora-VariableFont_wght.ttf + Lora-Italic-VariableFont_wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Lora: + static/Lora-Regular.ttf + static/Lora-Medium.ttf + static/Lora-SemiBold.ttf + static/Lora-Bold.ttf + static/Lora-Italic.ttf + static/Lora-MediumItalic.ttf + static/Lora-SemiBoldItalic.ttf + static/Lora-BoldItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/assets/fonts/Lora/static/Lora-Bold.ttf b/assets/fonts/Lora/static/Lora-Bold.ttf new file mode 100644 index 00000000..530c9e11 Binary files /dev/null and b/assets/fonts/Lora/static/Lora-Bold.ttf differ diff --git a/assets/fonts/Lora/static/Lora-BoldItalic.ttf b/assets/fonts/Lora/static/Lora-BoldItalic.ttf new file mode 100644 index 00000000..6bcc76b0 Binary files /dev/null and b/assets/fonts/Lora/static/Lora-BoldItalic.ttf differ diff --git a/assets/fonts/Lora/static/Lora-Italic.ttf b/assets/fonts/Lora/static/Lora-Italic.ttf new file mode 100644 index 00000000..d93bc5fc Binary files /dev/null and b/assets/fonts/Lora/static/Lora-Italic.ttf differ diff --git a/assets/fonts/Lora/static/Lora-Medium.ttf b/assets/fonts/Lora/static/Lora-Medium.ttf new file mode 100644 index 00000000..85ca5a27 Binary files /dev/null and b/assets/fonts/Lora/static/Lora-Medium.ttf differ diff --git a/assets/fonts/Lora/static/Lora-MediumItalic.ttf b/assets/fonts/Lora/static/Lora-MediumItalic.ttf new file mode 100644 index 00000000..42208fbe Binary files /dev/null and b/assets/fonts/Lora/static/Lora-MediumItalic.ttf differ diff --git a/assets/fonts/Lora/static/Lora-Regular.ttf b/assets/fonts/Lora/static/Lora-Regular.ttf new file mode 100644 index 00000000..2b1dab45 Binary files /dev/null and b/assets/fonts/Lora/static/Lora-Regular.ttf differ diff --git a/assets/fonts/Lora/static/Lora-SemiBold.ttf b/assets/fonts/Lora/static/Lora-SemiBold.ttf new file mode 100644 index 00000000..3a7c6d75 Binary files /dev/null and b/assets/fonts/Lora/static/Lora-SemiBold.ttf differ diff --git a/assets/fonts/Lora/static/Lora-SemiBoldItalic.ttf b/assets/fonts/Lora/static/Lora-SemiBoldItalic.ttf new file mode 100644 index 00000000..16c8254d Binary files /dev/null and b/assets/fonts/Lora/static/Lora-SemiBoldItalic.ttf differ diff --git a/assets/fonts/Open_Sans/OFL.txt b/assets/fonts/Open_Sans/OFL.txt new file mode 100644 index 00000000..4fc61702 --- /dev/null +++ b/assets/fonts/Open_Sans/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/Open_Sans/README.txt b/assets/fonts/Open_Sans/README.txt new file mode 100644 index 00000000..2548322c --- /dev/null +++ b/assets/fonts/Open_Sans/README.txt @@ -0,0 +1,100 @@ +Open Sans Variable Font +======================= + +This download contains Open Sans as both variable fonts and static fonts. + +Open Sans is a variable font with these axes: + wdth + wght + +This means all the styles are contained in these files: + OpenSans-VariableFont_wdth,wght.ttf + OpenSans-Italic-VariableFont_wdth,wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Open Sans: + static/OpenSans_Condensed-Light.ttf + static/OpenSans_Condensed-Regular.ttf + static/OpenSans_Condensed-Medium.ttf + static/OpenSans_Condensed-SemiBold.ttf + static/OpenSans_Condensed-Bold.ttf + static/OpenSans_Condensed-ExtraBold.ttf + static/OpenSans_SemiCondensed-Light.ttf + static/OpenSans_SemiCondensed-Regular.ttf + static/OpenSans_SemiCondensed-Medium.ttf + static/OpenSans_SemiCondensed-SemiBold.ttf + static/OpenSans_SemiCondensed-Bold.ttf + static/OpenSans_SemiCondensed-ExtraBold.ttf + static/OpenSans-Light.ttf + static/OpenSans-Regular.ttf + static/OpenSans-Medium.ttf + static/OpenSans-SemiBold.ttf + static/OpenSans-Bold.ttf + static/OpenSans-ExtraBold.ttf + static/OpenSans_Condensed-LightItalic.ttf + static/OpenSans_Condensed-Italic.ttf + static/OpenSans_Condensed-MediumItalic.ttf + static/OpenSans_Condensed-SemiBoldItalic.ttf + static/OpenSans_Condensed-BoldItalic.ttf + static/OpenSans_Condensed-ExtraBoldItalic.ttf + static/OpenSans_SemiCondensed-LightItalic.ttf + static/OpenSans_SemiCondensed-Italic.ttf + static/OpenSans_SemiCondensed-MediumItalic.ttf + static/OpenSans_SemiCondensed-SemiBoldItalic.ttf + static/OpenSans_SemiCondensed-BoldItalic.ttf + static/OpenSans_SemiCondensed-ExtraBoldItalic.ttf + static/OpenSans-LightItalic.ttf + static/OpenSans-Italic.ttf + static/OpenSans-MediumItalic.ttf + static/OpenSans-SemiBoldItalic.ttf + static/OpenSans-BoldItalic.ttf + static/OpenSans-ExtraBoldItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/assets/fonts/Open_Sans/static/OpenSans-Bold.ttf b/assets/fonts/Open_Sans/static/OpenSans-Bold.ttf new file mode 100644 index 00000000..98c74e0a Binary files /dev/null and b/assets/fonts/Open_Sans/static/OpenSans-Bold.ttf differ diff --git a/assets/fonts/Open_Sans/static/OpenSans-BoldItalic.ttf b/assets/fonts/Open_Sans/static/OpenSans-BoldItalic.ttf new file mode 100644 index 00000000..85589283 Binary files /dev/null and b/assets/fonts/Open_Sans/static/OpenSans-BoldItalic.ttf differ diff --git a/assets/fonts/Open_Sans/static/OpenSans-ExtraBold.ttf b/assets/fonts/Open_Sans/static/OpenSans-ExtraBold.ttf new file mode 100644 index 00000000..4eb33935 Binary files /dev/null and b/assets/fonts/Open_Sans/static/OpenSans-ExtraBold.ttf differ diff --git a/assets/fonts/Open_Sans/static/OpenSans-ExtraBoldItalic.ttf b/assets/fonts/Open_Sans/static/OpenSans-ExtraBoldItalic.ttf new file mode 100644 index 00000000..75789b42 Binary files /dev/null and b/assets/fonts/Open_Sans/static/OpenSans-ExtraBoldItalic.ttf differ diff --git a/assets/fonts/Open_Sans/static/OpenSans-Italic.ttf b/assets/fonts/Open_Sans/static/OpenSans-Italic.ttf new file mode 100644 index 00000000..29ff6938 Binary files /dev/null and b/assets/fonts/Open_Sans/static/OpenSans-Italic.ttf differ diff --git a/assets/fonts/Open_Sans/static/OpenSans-Light.ttf b/assets/fonts/Open_Sans/static/OpenSans-Light.ttf new file mode 100644 index 00000000..ea175cc3 Binary files /dev/null and b/assets/fonts/Open_Sans/static/OpenSans-Light.ttf differ diff --git a/assets/fonts/Open_Sans/static/OpenSans-LightItalic.ttf b/assets/fonts/Open_Sans/static/OpenSans-LightItalic.ttf new file mode 100644 index 00000000..edbfe0b7 Binary files /dev/null and b/assets/fonts/Open_Sans/static/OpenSans-LightItalic.ttf differ diff --git a/assets/fonts/Open_Sans/static/OpenSans-Medium.ttf b/assets/fonts/Open_Sans/static/OpenSans-Medium.ttf new file mode 100644 index 00000000..ae716936 Binary files /dev/null and b/assets/fonts/Open_Sans/static/OpenSans-Medium.ttf differ diff --git a/assets/fonts/Open_Sans/static/OpenSans-MediumItalic.ttf b/assets/fonts/Open_Sans/static/OpenSans-MediumItalic.ttf new file mode 100644 index 00000000..6d1e09b2 Binary files /dev/null and b/assets/fonts/Open_Sans/static/OpenSans-MediumItalic.ttf differ diff --git a/assets/fonts/Open_Sans/static/OpenSans-Regular.ttf b/assets/fonts/Open_Sans/static/OpenSans-Regular.ttf new file mode 100644 index 00000000..67803bb6 Binary files /dev/null and b/assets/fonts/Open_Sans/static/OpenSans-Regular.ttf differ diff --git a/assets/fonts/Open_Sans/static/OpenSans-SemiBold.ttf b/assets/fonts/Open_Sans/static/OpenSans-SemiBold.ttf new file mode 100644 index 00000000..e5ab4644 Binary files /dev/null and b/assets/fonts/Open_Sans/static/OpenSans-SemiBold.ttf differ diff --git a/assets/fonts/Open_Sans/static/OpenSans-SemiBoldItalic.ttf b/assets/fonts/Open_Sans/static/OpenSans-SemiBoldItalic.ttf new file mode 100644 index 00000000..cd23e154 Binary files /dev/null and b/assets/fonts/Open_Sans/static/OpenSans-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..eea7da85 --- /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/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 00000000..398842ca --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,37 @@ +PODS: + - Flutter (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - sqflite (0.0.3): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - Flutter (from `Flutter`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + sqflite: + :path: ".symlinks/plugins/sqflite/darwin" + +SPEC CHECKSUMS: + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + +PODFILE CHECKSUM: 0805b11bfb13bd44fc55fe52946ce14f22a2998e + +COCOAPODS: 1.14.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 73cf3f6d..1ecc4d32 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + F92F283DDC9D94160F3F516B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CCB819B2252C95B051824886 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -31,10 +32,12 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 324513623FCCD566787FC66A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 86ECDE178A9FD68CCFF6F89A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -42,6 +45,8 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CCB819B2252C95B051824886 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D74497FA3959478CD45B0959 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -49,12 +54,32 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F92F283DDC9D94160F3F516B /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 04041DF2FABE481943EE754F /* Pods */ = { + isa = PBXGroup; + children = ( + 86ECDE178A9FD68CCFF6F89A /* Pods-Runner.debug.xcconfig */, + D74497FA3959478CD45B0959 /* Pods-Runner.release.xcconfig */, + 324513623FCCD566787FC66A /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 49C28CAA1F7BD65765B44672 /* Frameworks */ = { + isa = PBXGroup; + children = ( + CCB819B2252C95B051824886 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +97,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 04041DF2FABE481943EE754F /* Pods */, + 49C28CAA1F7BD65765B44672 /* Frameworks */, ); sourceTree = ""; }; @@ -105,12 +132,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + 8F88E781F6DA47829672036E /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + A90FC89CAF8D97082FB5E2B0 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -185,6 +214,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 8F88E781F6DA47829672036E /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -200,6 +251,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + A90FC89CAF8D97082FB5E2B0 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/lib/core/animations/animate_do_fades.dart b/lib/core/animations/animate_do_fades.dart new file mode 100644 index 00000000..e9d08a34 --- /dev/null +++ b/lib/core/animations/animate_do_fades.dart @@ -0,0 +1,702 @@ +import 'package:flutter/material.dart'; + +/// Class [FadeIn]: +/// [key]: optional widget key reference +/// [child]: mandatory, widget to animate +/// [duration]: how much time the animation should take +/// [delay]: delay before the animation starts +/// [controller]: optional/mandatory, exposes the animation controller created by Animate_do +/// [manualTrigger]: boolean that indicates if you want to trigger the animation manually with the controller +/// [animate]: For a State controller property, if you re-render changing it from false to true, the animation will be fired inmediatelly +class FadeIn extends StatefulWidget { + final Widget child; + final Duration duration; + final Duration delay; + final Function(AnimationController)? controller; + final bool manualTrigger; + final bool animate; + + FadeIn({ + super.key, + required this.child, + this.duration = const Duration(milliseconds: 500), + this.delay = const Duration(milliseconds: 0), + this.controller, + this.manualTrigger = false, + this.animate = true, + }) { + if (manualTrigger == true && controller == null) { + throw FlutterError('If you want to use manualTrigger:true, \n\n' + 'Then you must provide the controller property, that is a callback like:\n\n' + ' ( controller: AnimationController) => yourController = controller \n\n'); + } + } + + @override + FadeInState createState() => FadeInState(); +} + +/// FadeState class +/// The animation magic happens here +class FadeInState extends State with SingleTickerProviderStateMixin { + /// Animation controller that controls this animation + late AnimationController controller; + + /// is the widget disposed? + bool disposed = false; + + /// Animation movement value + late Animation animation; + + @override + void dispose() { + disposed = true; + controller.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + + controller = AnimationController(duration: widget.duration, vsync: this); + animation = CurvedAnimation(curve: Curves.easeOut, parent: controller); + + if (!widget.manualTrigger && widget.animate) { + Future.delayed(widget.delay, () { + if (!disposed) { + controller.forward(); + } + }); + } + + if (widget.controller is Function) { + widget.controller!(controller); + } + } + + @override + Widget build(BuildContext context) { + /// Launch the animation ASAP or wait if is needed + if (widget.animate && widget.delay.inMilliseconds == 0 && widget.manualTrigger == false) { + controller.forward(); + } + + /// If the animation already happen, we can animate it back + if (!widget.animate) { + controller.animateBack(0); + } + + /// Builds the animation with the corresponding + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + return Opacity( + opacity: animation.value, + child: widget.child, + ); + }, + ); + } +} + +/// Class [FadeInDown]: +/// [key]: optional widget key reference +/// [child]: mandatory, widget to animate +/// [duration]: how much time the animation should take +/// [delay]: delay before the animation starts +/// [controller]: optional/mandatory, exposes the animation controller created by Animate_do +/// the controller can be use to repeat, reverse and anything you want, its just an animation controller +class FadeInDown extends StatefulWidget { + final Widget child; + final Duration duration; + final Duration delay; + final Function(AnimationController)? controller; + final bool manualTrigger; + final bool animate; + final double from; + + FadeInDown({ + super.key, + required this.child, + this.duration = const Duration(milliseconds: 800), + this.delay = const Duration(milliseconds: 0), + this.controller, + this.manualTrigger = false, + this.animate = true, + this.from = 100, + }) { + if (manualTrigger == true && controller == null) { + throw FlutterError('If you want to use manualTrigger:true, \n\n' + 'Then you must provide the controller property, that is a callback like:\n\n' + ' ( controller: AnimationController) => yourController = controller \n\n'); + } + } + + @override + FadeInDownState createState() => FadeInDownState(); +} + +/// FadeState class +/// The animation magic happens here +class FadeInDownState extends State with SingleTickerProviderStateMixin { + late AnimationController controller; + + /// is the widget disposed? + bool disposed = false; + + /// animation movement + late Animation animation; + + /// animation opacity + late Animation opacity; + + @override + void dispose() { + disposed = true; + controller.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + + controller = AnimationController(duration: widget.duration, vsync: this); + + animation = Tween(begin: widget.from * -1, end: 0) + .animate(CurvedAnimation(parent: controller, curve: Curves.easeOut)); + + opacity = + Tween(begin: 0, end: 1).animate(CurvedAnimation(parent: controller, curve: const Interval(0, 0.65))); + + if (!widget.manualTrigger && widget.animate) { + Future.delayed(widget.delay, () { + if (!disposed) { + controller.forward(); + } + }); + } + + /// Returns the controller if the user requires it + if (widget.controller is Function) { + widget.controller!(controller); + } + } + + @override + Widget build(BuildContext context) { + if (widget.animate && widget.delay.inMilliseconds == 0 && widget.manualTrigger == false) { + controller.forward(); + } + + /// If FALSE, animate everything back to the original state + if (!widget.animate) { + controller.animateBack(0); + } + + return AnimatedBuilder( + animation: controller, + builder: (BuildContext context, Widget? child) { + return Transform.translate( + offset: Offset(0, animation.value), + child: Opacity( + opacity: opacity.value, + child: widget.child, + ), + ); + }, + ); + } +} + +/// Class [FadeInDownBig]: +/// [key]: optional widget key reference +/// [child]: mandatory, widget to animate +/// [duration]: how much time the animation should take +/// [delay]: delay before the animation starts +/// [controller]: optional/mandatory, exposes the animation controller created by Animate_do +/// the controller can be use to repeat, reverse and anything you want, its just an animation controller +class FadeInDownBig extends StatelessWidget { + final Widget child; + final Duration duration; + final Duration delay; + final Function(AnimationController)? controller; + final bool manualTrigger; + final bool animate; + final double from; + + FadeInDownBig({ + super.key, + required this.child, + this.duration = const Duration(milliseconds: 1300), + this.delay = const Duration(milliseconds: 0), + this.controller, + this.manualTrigger = false, + this.animate = true, + this.from = 600, + }) { + if (manualTrigger == true && controller == null) { + throw FlutterError('If you want to use manualTrigger:true, \n\n' + 'Then you must provide the controller property, that is a callback like:\n\n' + ' ( controller: AnimationController) => yourController = controller \n\n'); + } + } + + @override + Widget build(BuildContext context) => FadeInDown( + duration: duration, + delay: delay, + controller: controller, + manualTrigger: manualTrigger, + animate: animate, + from: from, + child: child, + ); +} + +/// Class [FadeInUp]: +/// [key]: optional widget key reference +/// [child]: mandatory, widget to animate +/// [duration]: how much time the animation should take +/// [delay]: delay before the animation starts +/// [controller]: optional/mandatory, exposes the animation controller created by Animate_do +/// the controller can be use to repeat, reverse and anything you want, its just an animation controller +class FadeInUp extends StatefulWidget { + final Widget child; + final Duration duration; + final Duration delay; + final Function(AnimationController)? controller; + final bool manualTrigger; + final bool animate; + final double from; + + FadeInUp({ + super.key, + required this.child, + this.duration = const Duration(milliseconds: 800), + this.delay = const Duration(milliseconds: 0), + this.controller, + this.manualTrigger = false, + this.animate = true, + this.from = 100, + }) { + if (manualTrigger == true && controller == null) { + throw FlutterError('If you want to use manualTrigger:true, \n\n' + 'Then you must provide the controller property, that is a callback like:\n\n' + ' ( controller: AnimationController) => yourController = controller \n\n'); + } + } + + @override + FadeInUpState createState() => FadeInUpState(); +} + +/// FadeState class +/// The animation magic happens here +class FadeInUpState extends State with SingleTickerProviderStateMixin { + /// Animation controller if requested + late AnimationController controller; + + /// widget is disposed? + bool disposed = false; + + /// Animation movement + late Animation animation; + + /// Animation opacity + late Animation opacity; + + @override + void dispose() { + disposed = true; + controller.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + + controller = AnimationController(duration: widget.duration, vsync: this); + + animation = + Tween(begin: widget.from, end: 0).animate(CurvedAnimation(parent: controller, curve: Curves.easeOut)); + opacity = + Tween(begin: 0, end: 1).animate(CurvedAnimation(parent: controller, curve: const Interval(0, 0.65))); + + if (!widget.manualTrigger && widget.animate) { + Future.delayed(widget.delay, () { + if (!disposed) { + controller.forward(); + } + }); + } + + if (widget.controller is Function) { + widget.controller!(controller); + } + } + + @override + Widget build(BuildContext context) { + if (widget.animate && widget.delay.inMilliseconds == 0 && widget.manualTrigger == false) { + controller.forward(); + } + + /// If FALSE, animate everything back to the original state + if (!widget.animate) { + controller.animateBack(0); + } + + return AnimatedBuilder( + animation: controller, + builder: (BuildContext context, Widget? child) { + return Transform.translate( + offset: Offset(0, animation.value), + child: Opacity( + opacity: opacity.value, + child: widget.child, + ), + ); + }, + ); + } +} + +/// Class [FadeInUpBig]: +/// [key]: optional widget key reference +/// [child]: mandatory, widget to animate +/// [duration]: how much time the animation should take +/// [delay]: delay before the animation starts +/// [controller]: optional/mandatory, exposes the animation controller created by Animate_do +/// the controller can be use to repeat, reverse and anything you want, its just an animation controller +class FadeInUpBig extends StatelessWidget { + final Widget child; + final Duration duration; + final Duration delay; + final Function(AnimationController)? controller; + final bool manualTrigger; + final bool animate; + final double from; + + FadeInUpBig({ + super.key, + required this.child, + this.duration = const Duration(milliseconds: 1300), + this.delay = const Duration(milliseconds: 0), + this.controller, + this.manualTrigger = false, + this.animate = true, + this.from = 600, + }) { + if (manualTrigger == true && controller == null) { + throw FlutterError('If you want to use manualTrigger:true, \n\n' + 'Then you must provide the controller property, that is a callback like:\n\n' + ' ( controller: AnimationController) => yourController = controller \n\n'); + } + } + + @override + Widget build(BuildContext context) => FadeInUp( + duration: duration, + delay: delay, + controller: controller, + manualTrigger: manualTrigger, + animate: animate, + from: from, + child: child, + ); +} + +/// Class [FadeInLeft]: +/// [key]: optional widget key reference +/// [child]: mandatory, widget to animate +/// [duration]: how much time the animation should take +/// [delay]: delay before the animation starts +/// [controller]: optional/mandatory, exposes the animation controller created by Animate_do +/// the controller can be use to repeat, reverse and anything you want, its just an animation controller +class FadeInLeft extends StatefulWidget { + final Widget child; + final Duration duration; + final Duration delay; + final Function(AnimationController)? controller; + final bool manualTrigger; + final bool animate; + final double from; + + FadeInLeft({ + super.key, + required this.child, + this.duration = const Duration(milliseconds: 800), + this.delay = const Duration(milliseconds: 0), + this.controller, + this.manualTrigger = false, + this.animate = true, + this.from = 100, + }) { + if (manualTrigger == true && controller == null) { + throw FlutterError('If you want to use manualTrigger:true, \n\n' + 'Then you must provide the controller property, that is a callback like:\n\n' + ' ( controller: AnimationController) => yourController = controller \n\n'); + } + } + + @override + FadeInLeftState createState() => FadeInLeftState(); +} + +/// FadeState class +/// The animation magic happens here +class FadeInLeftState extends State with SingleTickerProviderStateMixin { + late AnimationController controller; + bool disposed = false; + late Animation animation; + late Animation opacity; + @override + void dispose() { + disposed = true; + controller.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + + controller = AnimationController(duration: widget.duration, vsync: this); + + animation = Tween(begin: widget.from * -1, end: 0) + .animate(CurvedAnimation(parent: controller, curve: Curves.easeOut)); + opacity = + Tween(begin: 0, end: 1).animate(CurvedAnimation(parent: controller, curve: const Interval(0, 0.65))); + + if (!widget.manualTrigger && widget.animate) { + Future.delayed(widget.delay, () { + if (!disposed) { + controller.forward(); + } + }); + } + + if (widget.controller is Function) { + widget.controller!(controller); + } + } + + @override + Widget build(BuildContext context) { + if (widget.animate && widget.delay.inMilliseconds == 0 && widget.manualTrigger == false) { + controller.forward(); + } + + /// If FALSE, animate everything back to the original state + if (!widget.animate) { + controller.animateBack(0); + } + + return AnimatedBuilder( + animation: controller, + builder: (BuildContext context, Widget? child) { + return Transform.translate( + offset: Offset(animation.value, 0), + child: Opacity( + opacity: opacity.value, + child: widget.child, + ), + ); + }, + ); + } +} + +/// Class [FadeInLeftBig]: +/// [key]: optional widget key reference +/// [child]: mandatory, widget to animate +/// [duration]: how much time the animation should take +/// [delay]: delay before the animation starts +/// [controller]: optional/mandatory, exposes the animation controller created by Animate_do +/// the controller can be use to repeat, reverse and anything you want, its just an animation controller +class FadeInLeftBig extends StatelessWidget { + final Widget child; + final Duration duration; + final Duration delay; + final Function(AnimationController)? controller; + final bool manualTrigger; + final bool animate; + final double from; + + FadeInLeftBig({ + super.key, + required this.child, + this.duration = const Duration(milliseconds: 1300), + this.delay = const Duration(milliseconds: 0), + this.controller, + this.manualTrigger = false, + this.animate = true, + this.from = 600, + }) { + if (manualTrigger == true && controller == null) { + throw FlutterError('If you want to use manualTrigger:true, \n\n' + 'Then you must provide the controller property, that is a callback like:\n\n' + ' ( controller: AnimationController) => yourController = controller \n\n'); + } + } + + @override + Widget build(BuildContext context) => FadeInLeft( + duration: duration, + delay: delay, + controller: controller, + manualTrigger: manualTrigger, + animate: animate, + from: from, + child: child, + ); +} + +/// Class [FadeInRight]: +/// [key]: optional widget key reference +/// [child]: mandatory, widget to animate +/// [duration]: how much time the animation should take +/// [delay]: delay before the animation starts +/// [controller]: optional/mandatory, exposes the animation controller created by Animate_do +/// the controller can be use to repeat, reverse and anything you want, its just an animation controller +class FadeInRight extends StatefulWidget { + final Widget child; + final Duration duration; + final Duration delay; + final Function(AnimationController)? controller; + final bool manualTrigger; + final bool animate; + final double from; + + FadeInRight({ + super.key, + required this.child, + this.duration = const Duration(milliseconds: 800), + this.delay = const Duration(milliseconds: 0), + this.controller, + this.manualTrigger = false, + this.animate = true, + this.from = 100, + }) { + if (manualTrigger == true && controller == null) { + throw FlutterError('If you want to use manualTrigger:true, \n\n' + 'Then you must provide the controller property, that is a callback like:\n\n' + ' ( controller: AnimationController) => yourController = controller \n\n'); + } + } + + @override + FadeInRightState createState() => FadeInRightState(); +} + +/// FadeState class +/// The animation magic happens here +class FadeInRightState extends State with SingleTickerProviderStateMixin { + late AnimationController controller; + bool disposed = false; + late Animation animation; + late Animation opacity; + @override + void dispose() { + disposed = true; + controller.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + + controller = AnimationController(duration: widget.duration, vsync: this); + + animation = + Tween(begin: widget.from, end: 0).animate(CurvedAnimation(parent: controller, curve: Curves.easeOut)); + opacity = + Tween(begin: 0, end: 1).animate(CurvedAnimation(parent: controller, curve: const Interval(0, 0.65))); + + if (!widget.manualTrigger && widget.animate) { + Future.delayed(widget.delay, () { + if (!disposed) { + controller.forward(); + } + }); + } + + if (widget.controller is Function) { + widget.controller!(controller); + } + } + + @override + Widget build(BuildContext context) { + if (widget.animate && widget.delay.inMilliseconds == 0 && widget.manualTrigger == false) { + controller.forward(); + } + + /// If FALSE, animate everything back to the original state + if (!widget.animate) { + controller.animateBack(0); + } + + return AnimatedBuilder( + animation: controller, + builder: (BuildContext context, Widget? child) { + return Transform.translate( + offset: Offset(animation.value, 0), + child: Opacity( + opacity: opacity.value, + child: widget.child, + ), + ); + }, + ); + } +} + +/// Class [FadeInRightBig]: +/// [key]: optional widget key reference +/// [child]: mandatory, widget to animate +/// [duration]: how much time the animation should take +/// [delay]: delay before the animation starts +/// [controller]: optional/mandatory, exposes the animation controller created by Animate_do +/// the controller can be use to repeat, reverse and anything you want, its just an animation controller +class FadeInRightBig extends StatelessWidget { + final Widget child; + final Duration duration; + final Duration delay; + final Function(AnimationController)? controller; + final bool manualTrigger; + final bool animate; + final double from; + + FadeInRightBig({ + super.key, + required this.child, + this.duration = const Duration(milliseconds: 1200), + this.delay = const Duration(milliseconds: 0), + this.controller, + this.manualTrigger = false, + this.animate = true, + this.from = 600, + }) { + if (manualTrigger == true && controller == null) { + throw FlutterError('If you want to use manualTrigger:true, \n\n' + 'Then you must provide the controller property, that is a callback like:\n\n' + ' ( controller: AnimationController) => yourController = controller \n\n'); + } + } + + @override + Widget build(BuildContext context) => FadeInRight( + duration: duration, + delay: delay, + controller: controller, + manualTrigger: manualTrigger, + animate: animate, + from: from, + child: child, + ); +} diff --git a/lib/core/enums/status_type_enum.dart b/lib/core/enums/status_type_enum.dart new file mode 100644 index 00000000..f274d7d9 --- /dev/null +++ b/lib/core/enums/status_type_enum.dart @@ -0,0 +1,22 @@ +enum StatusType { + idle, + loading, + loadingMore, + success, + failure, + finished, + empty; + + bool get isIdle => this == StatusType.idle; + bool get isLoading => this == StatusType.loading; + bool get isLoadingMore => this == StatusType.loadingMore; + bool get isLoadingOrLoadingMore => isLoading || isLoadingMore; + bool get isLoadingActiveOrFinished => isLoading || isLoadingMore || isFinished; + bool get isLoadingActiveOrEmpty => isLoading || isLoadingMore || isEmpty; + bool get isSuccess => this == StatusType.success; + bool get isFinished => this == StatusType.finished; + bool get isSuccessOrFinished => isSuccess || isFinished; + bool get isSuccessOrFinishedOrEmpty => isSuccessOrFinished || isEmpty; + bool get isFailure => this == StatusType.failure; + bool get isEmpty => this == StatusType.empty; +} diff --git a/lib/core/services/local_storage/local_storage.dart b/lib/core/services/local_storage/local_storage.dart new file mode 100644 index 00000000..ec686c49 --- /dev/null +++ b/lib/core/services/local_storage/local_storage.dart @@ -0,0 +1,120 @@ +import 'dart:async'; + +import 'package:shared_preferences/shared_preferences.dart'; + +import 'local_storage_interface.dart'; + +class LocalStorageService implements ILocalStorageService { + SharedPreferences? _prefs; + + LocalStorageService._internal(); + + factory LocalStorageService() => LocalStorageService._internal(); + + Future get _initializePrefs async { + _prefs ??= await SharedPreferences.getInstance(); + return _prefs; + } + + @override + Future clear() async { + final pref = await _initializePrefs; + pref?.clear(); + } + + @override + Future getBool(String key) async { + final pref = await _initializePrefs; + return pref?.getBool(key); + } + + @override + Future getInt(String key) async { + final pref = await _initializePrefs; + return pref?.getInt(key); + } + + @override + Future getDouble(String key) async { + final pref = await _initializePrefs; + return pref?.getDouble(key); + } + + @override + Future getString(String key) async { + final pref = await _initializePrefs; + return pref?.getString(key); + } + + @override + Future setBool(String key, bool? value) async { + final pref = await _initializePrefs; + + if (value == null) { + pref?.remove(key); + return; + } + + pref?.setBool(key, value); + } + + @override + Future setInt(String key, int? value) async { + final pref = await _initializePrefs; + + if (value == null) { + pref?.remove(key); + return; + } + + pref?.setInt(key, value); + } + + @override + Future setDouble(String key, double? value) async { + final pref = await _initializePrefs; + + if (value == null) { + pref?.remove(key); + return; + } + + pref?.setDouble(key, value); + } + + @override + Future setString(String key, String? value) async { + final pref = await _initializePrefs; + + if (value == null) { + pref?.remove(key); + return; + } + + pref?.setString(key, value); + } + + @override + Future remove(String key) async { + final pref = await _initializePrefs; + pref?.remove(key); + } + + @override + Future?> getStringList(String key) async { + final pref = await _initializePrefs; + return pref?.getStringList(key); + } + + @override + Future setStringList(String key, List? value) async { + final pref = await _initializePrefs; + + if (value == null) { + pref?.remove(key); + return; + } + + pref?.setStringList(key, value); + } +} diff --git a/lib/core/services/local_storage/local_storage_interface.dart b/lib/core/services/local_storage/local_storage_interface.dart new file mode 100644 index 00000000..cd23a1fa --- /dev/null +++ b/lib/core/services/local_storage/local_storage_interface.dart @@ -0,0 +1,25 @@ +abstract class ILocalStorageService { + Future clear(); + + Future getBool(String key); + + Future getInt(String key); + + Future getDouble(String key); + + Future getString(String key); + + Future setBool(String key, bool value); + + Future setInt(String key, int? value); + + Future setDouble(String key, double value); + + Future setString(String key, String value); + + Future remove(String key); + + Future?> getStringList(String key); + + Future setStringList(String key, List value); +} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart new file mode 100644 index 00000000..0e88530d --- /dev/null +++ b/lib/core/theme/app_theme.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:restaurantour/core/theme/text_theme.dart'; + +class AppTheme { + AppTheme._(); + + // * Colors default app + static const Color primaryColor = Color(0xFF000000); + static const Color secondary = Color(0xFF606060); + + // * Custom colors + static const Color statusBarColor = Colors.white; + static const Color backgroundColor = Color(0xffFAFAFA); + static const Color greenColor = Color(0xFF5CD313); + static const Color redColor = Color(0xFFEA5E5E); + static const Color yellowColor = Color(0xFFFFB800); + + static ThemeData get defaultTheme => ThemeData( + primaryColor: primaryColor, + fontFamily: 'Lora', + appBarTheme: const AppBarTheme( + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: statusBarColor, + statusBarBrightness: Brightness.light, + statusBarIconBrightness: Brightness.dark, + ), + surfaceTintColor: Colors.white, + foregroundColor: Colors.black, + shadowColor: Colors.black54, + color: Colors.white, + elevation: 3, + centerTitle: true, + titleTextStyle: AppTextStyle.black18w700, + ), + tabBarTheme: const TabBarTheme( + labelStyle: AppTextStyle.black14w600OpenSans, + unselectedLabelStyle: AppTextStyle.black14w600OpenSans, + indicatorColor: Colors.black, + labelColor: Colors.black, + unselectedLabelColor: Colors.black54, + tabAlignment: TabAlignment.center, + splashFactory: NoSplash.splashFactory, + overlayColor: MaterialStatePropertyAll(Colors.transparent), + ), + scaffoldBackgroundColor: backgroundColor, + visualDensity: VisualDensity.adaptivePlatformDensity, + colorScheme: const ColorScheme.light( + secondary: secondary, + primary: primaryColor, + background: backgroundColor, + surfaceTint: backgroundColor, + ), + dividerTheme: const DividerThemeData(color: Color(0xFFEEEEEE), thickness: 1), + useMaterial3: true, + ); +} diff --git a/lib/core/theme/text_theme.dart b/lib/core/theme/text_theme.dart new file mode 100644 index 00000000..fd856220 --- /dev/null +++ b/lib/core/theme/text_theme.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +class AppTextStyle { + AppTextStyle._(); + + static const String _fontFamily = 'Lora'; + static const String _fontFamilyOpenSans = 'Open Sans'; + + static const black12w400OpenSans = TextStyle( + fontSize: 12, + fontWeight: FontWeight.normal, + color: Colors.black, + fontFamily: _fontFamilyOpenSans, + ); + + static const black14w400 = TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + fontFamily: _fontFamily, + ); + + static const black14w400OpenSans = TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black, + fontFamily: _fontFamilyOpenSans, + ); + + static const black14w600 = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.black, + fontFamily: _fontFamily, + ); + + static const black14w600OpenSans = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black, + fontFamily: _fontFamilyOpenSans, + ); + + static const black16w600 = TextStyle( + fontSize: 16, + color: Colors.black, + fontWeight: FontWeight.w600, + fontFamily: _fontFamily, + ); + + static const black18w700 = TextStyle( + fontSize: 18, + color: Colors.black, + fontWeight: FontWeight.w700, + fontFamily: _fontFamily, + ); + + static const white16w600 = TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w600, + fontFamily: _fontFamily, + ); + + static const white18w600 = TextStyle( + fontSize: 18, + color: Colors.white, + fontWeight: FontWeight.w600, + fontFamily: _fontFamily, + ); +} diff --git a/lib/core/utils/constants.dart b/lib/core/utils/constants.dart new file mode 100644 index 00000000..b16b54bd --- /dev/null +++ b/lib/core/utils/constants.dart @@ -0,0 +1,12 @@ +class ConstantsApp { + ConstantsApp._(); + + static const String localFavorites = 'local_favorites'; + + static const String kLoading = 'loading_widget'; + static const String kErrorDetailsRestaurant = 'error_details_restaurant'; + static const String kTitleDetailsRestaurant = 'title_details_restaurant'; + static const String kReviewsRestaurant = 'reviews_restaurant'; + static const String kCustomScrollHomePage = 'custom_scroll_home'; + static const String kCardRestaurant = 'card_restaurant'; +} diff --git a/lib/core/utils/custom_logger.dart b/lib/core/utils/custom_logger.dart new file mode 100644 index 00000000..ed2c2d4f --- /dev/null +++ b/lib/core/utils/custom_logger.dart @@ -0,0 +1,40 @@ +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; + +class LoggerApp { + LoggerApp._(); + + static void success(dynamic message) { + if (message is! String) message = message.toString(); + log('\x1B[32m$message\x1B[0m', name: 'restaurant_app', time: DateTime.now()); + } + + static void info(dynamic message) { + if (message is! String) message = message.toString(); + + if (kDebugMode) { + log('\x1B[34m$message\x1B[0m', name: 'restaurant_app', time: DateTime.now()); + } + } + + static void error(dynamic message, [Object? exception, StackTrace? stackTrace]) { + if (message is! String) message = message.toString(); + + if (kDebugMode) { + log('\x1B[31m$message\x1B[0m', name: 'restaurant_app', stackTrace: stackTrace, time: DateTime.now()); + } + } + + static void warning(dynamic message) { + if (message is! String) message = message.toString(); + if (kDebugMode) { + log('\x1B[33m$message\x1B[0m', name: 'restaurant_app', time: DateTime.now()); + } + } + + static void debug(dynamic message) { + if (message is! String) message = message.toString(); + if (kDebugMode) log(message, name: 'restaurant_app', time: DateTime.now()); + } +} diff --git a/lib/core/utils/dependency_injector.dart b/lib/core/utils/dependency_injector.dart new file mode 100644 index 00000000..5647dde3 --- /dev/null +++ b/lib/core/utils/dependency_injector.dart @@ -0,0 +1,23 @@ +import 'package:get_it/get_it.dart'; +import 'package:restaurantour/modules/home/data/repositories/yelp_repository.dart'; +import 'package:restaurantour/modules/home/domain/controllers/details_restaurant_controller.dart'; +import 'package:restaurantour/modules/home/domain/controllers/home_controller.dart'; +import 'package:restaurantour/modules/home/domain/repositories/restaurant_repository_interface.dart'; +import 'package:restaurantour/modules/home/domain/stores/favorite_store.dart'; + +import '../services/local_storage/local_storage.dart'; +import '../services/local_storage/local_storage_interface.dart'; + +GetIt locator = GetIt.instance; + +Future setupServicesLocator() async { + locator.registerSingleton(LocalStorageService()); + locator.registerLazySingleton(() => FavoriteStore(locator.get())); + locator.registerSingleton(YelpRepository()); + locator.registerLazySingleton( + () => HomeController(locator.get(), locator.get()), + ); + locator.registerFactory( + () => DetailsRestaurantController(locator.get()), + ); +} diff --git a/lib/main.dart b/lib/main.dart index c6ce7473..6a172be1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:restaurantour/repositories/yelp_repository.dart'; +import 'package:restaurantour/core/theme/app_theme.dart'; +import 'package:restaurantour/core/utils/dependency_injector.dart'; + +import 'modules/home/ui/home_page.dart'; void main() { + // Initialize Flutter + WidgetsFlutterBinding.ensureInitialized(); + + // Injections + setupServicesLocator(); + runApp(const Restaurantour()); } @@ -13,45 +22,15 @@ class Restaurantour extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'RestauranTour', - theme: ThemeData( - visualDensity: VisualDensity.adaptivePlatformDensity, - ), + debugShowCheckedModeBanner: false, + theme: AppTheme.defaultTheme, 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'); - } - }, - ), - ], - ), - ), + builder: (context, child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + child: child!, + ); + }, ); } } diff --git a/lib/modules/home/data/models/response_model.dart b/lib/modules/home/data/models/response_model.dart new file mode 100644 index 00000000..2b9b8198 --- /dev/null +++ b/lib/modules/home/data/models/response_model.dart @@ -0,0 +1,14 @@ +class ResponseModel { + final D? data; + final E? error; + final int? statusCode; + + ResponseModel({ + this.data, + this.error, + this.statusCode, + }); + + bool get isSuccess => data != null; + bool get isError => error != null; +} diff --git a/lib/models/restaurant.dart b/lib/modules/home/data/models/restaurant.dart similarity index 89% rename from lib/models/restaurant.dart rename to lib/modules/home/data/models/restaurant.dart index 87c7aab5..232c7d99 100644 --- a/lib/models/restaurant.dart +++ b/lib/modules/home/data/models/restaurant.dart @@ -12,8 +12,7 @@ class Category { this.title, }); - factory Category.fromJson(Map json) => - _$CategoryFromJson(json); + factory Category.fromJson(Map json) => _$CategoryFromJson(json); Map toJson() => _$CategoryToJson(this); } @@ -53,11 +52,13 @@ class User { @JsonSerializable() class Review { final String? id; + final String? text; final int? rating; final User? user; const Review({ this.id, + this.text, this.rating, this.user, }); @@ -76,8 +77,7 @@ class Location { this.formattedAddress, }); - factory Location.fromJson(Map json) => - _$LocationFromJson(json); + factory Location.fromJson(Map json) => _$LocationFromJson(json); Map toJson() => _$LocationToJson(this); } @@ -106,8 +106,7 @@ class Restaurant { this.location, }); - factory Restaurant.fromJson(Map json) => - _$RestaurantFromJson(json); + factory Restaurant.fromJson(Map json) => _$RestaurantFromJson(json); Map toJson() => _$RestaurantToJson(this); @@ -148,8 +147,7 @@ class RestaurantQueryResult { this.restaurants, }); - factory RestaurantQueryResult.fromJson(Map json) => - _$RestaurantQueryResultFromJson(json); + factory RestaurantQueryResult.fromJson(Map json) => _$RestaurantQueryResultFromJson(json); Map toJson() => _$RestaurantQueryResultToJson(this); } diff --git a/lib/models/restaurant.g.dart b/lib/modules/home/data/models/restaurant.g.dart similarity index 98% rename from lib/models/restaurant.g.dart rename to lib/modules/home/data/models/restaurant.g.dart index 3ed33f9a..2b7065a9 100644 --- a/lib/models/restaurant.g.dart +++ b/lib/modules/home/data/models/restaurant.g.dart @@ -38,6 +38,7 @@ Map _$UserToJson(User instance) => { Review _$ReviewFromJson(Map json) => Review( id: json['id'] as String?, + text: json['text'] as String?, rating: json['rating'] as int?, user: json['user'] == null ? null @@ -46,6 +47,7 @@ Review _$ReviewFromJson(Map json) => Review( Map _$ReviewToJson(Review instance) => { 'id': instance.id, + 'text': instance.text, 'rating': instance.rating, 'user': instance.user, }; diff --git a/lib/modules/home/data/repositories/yelp_repository.dart b/lib/modules/home/data/repositories/yelp_repository.dart new file mode 100644 index 00000000..92ed51e5 --- /dev/null +++ b/lib/modules/home/data/repositories/yelp_repository.dart @@ -0,0 +1,115 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:restaurantour/core/utils/custom_logger.dart'; + +import '../../domain/errors/erros.dart'; +import '../../domain/repositories/restaurant_repository_interface.dart'; +import '../models/response_model.dart'; +import '../models/restaurant.dart'; + +/// ! In a real application, I usually use String.fromEnvironment to get keys for security, +/// ! but in this test I will leave it here to help you test my code +const _apiKey = + 'zbRXVjMzE2j_KmW9SWAyeiSCMc7WGO5HZ4u9yuWGd-VdTmLd9Wwk5Q8wENb1JU-7O8-PYrk9cNF-gDKBDrxO4E_lnLHlz16LHD4P_5_HXvwX7btbgf3kGV4EkfQaZnYx'; + +class YelpRepository implements IRestaurantRepository { + late Dio dio; + + YelpRepository({ + @visibleForTesting Dio? dio, + }) : dio = dio ?? + Dio( + BaseOptions( + baseUrl: 'https://api.yelp.com', + headers: { + 'Authorization': 'Bearer $_apiKey', + 'Content-Type': 'application/graphql', + }, + ), + ); + + @override + Future, Exception>> getRestaurants({int offset = 0}) async { + try { + final String query = ''' + query getRestaurants { + search(location: "Las Vegas",limit: 20, offset: $offset) { + total + business { + id + name + price + rating + photos + hours { + is_open_now + } + } + } + } + '''; + + final response = await dio.post>('/v3/graphql', data: query); + final result = RestaurantQueryResult.fromJson(response.data!['data']['search']); + return ResponseModel(data: result.restaurants ?? []); + } on DioException catch (e) { + /// When you reach the limit of requests in the api + if ([400, 403].contains(e.response?.statusCode)) return ResponseModel(data: []); + + return ResponseModel(error: Exception('Error on request when get restaurants')); + } catch (e, s) { + LoggerApp.error('Error on get restaurants', e, s); + return ResponseModel(error: Exception('Error on get restaurants')); + } + } + + @override + Future> getRestaurantById({required String id}) async { + try { + final String query = ''' + query getRestaurantById { + business(id: "$id") { + id + name + price + photos + rating + review_count + categories { + alias + title + } + hours { + is_open_now + } + location { + formatted_address + } + reviews { + id + rating + text + user { + id + image_url + name + } + } + } + } + '''; + + final response = await dio.post>('/v3/graphql', data: query); + final business = response.data?['data']?['business']; + if (business == null) { + return ResponseModel(error: NotFoundRestaurant()); + } else { + final data = Restaurant.fromJson(response.data!['data']['business']); + return ResponseModel(data: data); + } + } catch (e, s) { + LoggerApp.error('Error on get restaurant by id', e, s); + return ResponseModel(error: Exception('Error on get restaurant by id')); + } + } +} diff --git a/lib/modules/home/domain/controllers/details_restaurant_controller.dart b/lib/modules/home/domain/controllers/details_restaurant_controller.dart new file mode 100644 index 00000000..e4e73ebf --- /dev/null +++ b/lib/modules/home/domain/controllers/details_restaurant_controller.dart @@ -0,0 +1,58 @@ +import 'package:mobx/mobx.dart'; +import 'package:restaurantour/modules/home/domain/repositories/restaurant_repository_interface.dart'; + +import '../../../../core/enums/status_type_enum.dart'; +import '../../data/models/restaurant.dart'; + +part 'details_restaurant_controller.g.dart'; + +class DetailsRestaurantController = DetailsRestaurantControllerBase with _$DetailsRestaurantController; + +abstract class DetailsRestaurantControllerBase with Store { + final IRestaurantRepository _restaurantRepository; + DetailsRestaurantControllerBase(this._restaurantRepository); + + // * Variables + // * ---------------------------------------------------------------------------------------------------------------- + // * ---------------------------------------------------------------------------------------------------------------- + + /// Controls list of restaurants + late Restaurant restaurant; + + // * Observables + // * ---------------------------------------------------------------------------------------------------------------- + // * ---------------------------------------------------------------------------------------------------------------- + + /// Controls status of page + @observable + StatusType status = StatusType.idle; + + // * Actions + // * ---------------------------------------------------------------------------------------------------------------- + // * ---------------------------------------------------------------------------------------------------------------- + + /// Setup initial + Future loadRestaurant(Restaurant restaurantModel) { + restaurant = restaurantModel; + return _getInfosRestaurant(); + } + + /// Gets all restaurants + Future _getInfosRestaurant() async { + status = StatusType.loading; + + if (restaurant.id == null) { + status = StatusType.failure; + return; + } + + final result = await _restaurantRepository.getRestaurantById(id: restaurant.id!); + + if (result.isSuccess) { + restaurant = result.data!; + status = StatusType.success; + } else { + status = StatusType.failure; + } + } +} diff --git a/lib/modules/home/domain/controllers/details_restaurant_controller.g.dart b/lib/modules/home/domain/controllers/details_restaurant_controller.g.dart new file mode 100644 index 00000000..8560316d --- /dev/null +++ b/lib/modules/home/domain/controllers/details_restaurant_controller.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'details_restaurant_controller.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers + +mixin _$DetailsRestaurantController on DetailsRestaurantControllerBase, Store { + late final _$statusAtom = + Atom(name: 'DetailsRestaurantControllerBase.status', context: context); + + @override + StatusType get status { + _$statusAtom.reportRead(); + return super.status; + } + + @override + set status(StatusType value) { + _$statusAtom.reportWrite(value, super.status, () { + super.status = value; + }); + } + + @override + String toString() { + return ''' +status: ${status} + '''; + } +} diff --git a/lib/modules/home/domain/controllers/home_controller.dart b/lib/modules/home/domain/controllers/home_controller.dart new file mode 100644 index 00000000..f4bf2d9f --- /dev/null +++ b/lib/modules/home/domain/controllers/home_controller.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:mobx/mobx.dart'; +import 'package:restaurantour/core/enums/status_type_enum.dart'; +import 'package:restaurantour/modules/home/data/models/restaurant.dart'; +import 'package:restaurantour/modules/home/domain/stores/favorite_store.dart'; + +import '../repositories/restaurant_repository_interface.dart'; + +part 'home_controller.g.dart'; + +class HomeController = HomeControllerBase with _$HomeController; + +abstract class HomeControllerBase with Store { + final FavoriteStore _favoriteStore; + final IRestaurantRepository _restaurantRepository; + HomeControllerBase(this._favoriteStore, this._restaurantRepository); + + // * Controllers + // * ---------------------------------------------------------------------------------------------------------------- + // * ---------------------------------------------------------------------------------------------------------------- + + final ScrollController scrollController = ScrollController(); + + // * Variables + // * ---------------------------------------------------------------------------------------------------------------- + // * ---------------------------------------------------------------------------------------------------------------- + + /// Controls paginação + int pagination = -1; + + // * Observables + // * ---------------------------------------------------------------------------------------------------------------- + // * ---------------------------------------------------------------------------------------------------------------- + + /// Controls status of page + @observable + StatusType status = StatusType.idle; + + /// Controls list of restaurants + @observable + ObservableList restaurants = ObservableList(); + + @computed + List get restaurantsFavorits => _favoriteStore.restaurantsFavorits; + + // * Actions + // * ---------------------------------------------------------------------------------------------------------------- + // * ---------------------------------------------------------------------------------------------------------------- + + /// Setup initial + void loadingInfos() { + _favoriteStore.getFavorites(); + _getRestaurants(refresh: true); + scrollController.addListener(_scrollListener); + } + + /// Gets all restaurants + Future _getRestaurants({bool refresh = false}) async { + if (status.isLoadingActiveOrFinished) return; + + if (refresh) { + pagination = 0; + restaurants.clear(); + } else { + pagination += 1; + } + + status = restaurants.isEmpty ? StatusType.loading : StatusType.loadingMore; + + final result = await _restaurantRepository.getRestaurants(offset: pagination); + + if (result.isSuccess) { + restaurants.addAll(result.data!); + status = result.data!.isEmpty ? StatusType.finished : StatusType.success; + } else { + status = StatusType.failure; + } + } + + /// Listener on scroll page + void _scrollListener() { + if (scrollController.position.userScrollDirection == ScrollDirection.forward) return; + + final double triggerFetchMoreSize = 0.75 * scrollController.positions.last.maxScrollExtent; + + if (scrollController.position.pixels > triggerFetchMoreSize) _getRestaurants(); + } + + void dispose() { + scrollController.dispose(); + } +} diff --git a/lib/modules/home/domain/controllers/home_controller.g.dart b/lib/modules/home/domain/controllers/home_controller.g.dart new file mode 100644 index 00000000..a4b570fd --- /dev/null +++ b/lib/modules/home/domain/controllers/home_controller.g.dart @@ -0,0 +1,60 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'home_controller.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers + +mixin _$HomeController on HomeControllerBase, Store { + Computed>? _$restaurantsFavoritsComputed; + + @override + List get restaurantsFavorits => (_$restaurantsFavoritsComputed ??= + Computed>(() => super.restaurantsFavorits, + name: 'HomeControllerBase.restaurantsFavorits')) + .value; + + late final _$statusAtom = + Atom(name: 'HomeControllerBase.status', context: context); + + @override + StatusType get status { + _$statusAtom.reportRead(); + return super.status; + } + + @override + set status(StatusType value) { + _$statusAtom.reportWrite(value, super.status, () { + super.status = value; + }); + } + + late final _$restaurantsAtom = + Atom(name: 'HomeControllerBase.restaurants', context: context); + + @override + ObservableList get restaurants { + _$restaurantsAtom.reportRead(); + return super.restaurants; + } + + @override + set restaurants(ObservableList value) { + _$restaurantsAtom.reportWrite(value, super.restaurants, () { + super.restaurants = value; + }); + } + + @override + String toString() { + return ''' +status: ${status}, +restaurants: ${restaurants}, +restaurantsFavorits: ${restaurantsFavorits} + '''; + } +} diff --git a/lib/modules/home/domain/errors/erros.dart b/lib/modules/home/domain/errors/erros.dart new file mode 100644 index 00000000..51569fef --- /dev/null +++ b/lib/modules/home/domain/errors/erros.dart @@ -0,0 +1 @@ +class NotFoundRestaurant implements Exception {} diff --git a/lib/modules/home/domain/repositories/restaurant_repository_interface.dart b/lib/modules/home/domain/repositories/restaurant_repository_interface.dart new file mode 100644 index 00000000..1d65ec71 --- /dev/null +++ b/lib/modules/home/domain/repositories/restaurant_repository_interface.dart @@ -0,0 +1,7 @@ +import '../../data/models/response_model.dart'; +import '../../data/models/restaurant.dart'; + +abstract class IRestaurantRepository { + Future, Exception>> getRestaurants({int offset = 0}); + Future> getRestaurantById({required String id}); +} diff --git a/lib/modules/home/domain/stores/favorite_store.dart b/lib/modules/home/domain/stores/favorite_store.dart new file mode 100644 index 00000000..87f45fb1 --- /dev/null +++ b/lib/modules/home/domain/stores/favorite_store.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +import 'package:mobx/mobx.dart'; +import 'package:restaurantour/core/services/local_storage/local_storage_interface.dart'; +import 'package:restaurantour/core/utils/constants.dart'; + +import '../../data/models/restaurant.dart'; +part 'favorite_store.g.dart'; + +class FavoriteStore = FavoriteStoreBase with _$FavoriteStore; + +abstract class FavoriteStoreBase with Store { + final ILocalStorageService _localStorage; + FavoriteStoreBase(this._localStorage); + + // * Observables + // * ---------------------------------------------------------------------------------------------------------------- + // * ---------------------------------------------------------------------------------------------------------------- + + /// Controls list of restaurants + @observable + ObservableList restaurantsFavorits = ObservableList(); + + // * Actions + // * ---------------------------------------------------------------------------------------------------------------- + // * ---------------------------------------------------------------------------------------------------------------- + + /// Gets all favorites restaurants + Future getFavorites() async { + restaurantsFavorits.clear(); + final List response = await _localStorage.getStringList(ConstantsApp.localFavorites) ?? []; + if (response.isNotEmpty) { + final List data = response.map((element) => Restaurant.fromJson(jsonDecode(element))).toList(); + restaurantsFavorits.addAll(data); + } + } + + /// Adds a restaurant on list of favorites + void addFavorite(Restaurant restaurant) { + restaurantsFavorits.add(restaurant); + _updateListFavorites(); + } + + /// Removes a restaurant on list of favorites + void removeFavorite(Restaurant restaurant) { + _localStorage.remove(restaurant.id!); + restaurantsFavorits.removeWhere((element) => element.id == restaurant.id); + _updateListFavorites(); + } + + /// Updates a list of favorites + void _updateListFavorites() { + final List data = restaurantsFavorits.map((element) => jsonEncode(element.toJson())).toList(); + _localStorage.setStringList(ConstantsApp.localFavorites, data); + } +} diff --git a/lib/modules/home/domain/stores/favorite_store.g.dart b/lib/modules/home/domain/stores/favorite_store.g.dart new file mode 100644 index 00000000..3d2002c6 --- /dev/null +++ b/lib/modules/home/domain/stores/favorite_store.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'favorite_store.dart'; + +// ************************************************************************** +// StoreGenerator +// ************************************************************************** + +// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers + +mixin _$FavoriteStore on FavoriteStoreBase, Store { + late final _$restaurantsFavoritsAtom = + Atom(name: 'FavoriteStoreBase.restaurantsFavorits', context: context); + + @override + ObservableList get restaurantsFavorits { + _$restaurantsFavoritsAtom.reportRead(); + return super.restaurantsFavorits; + } + + @override + set restaurantsFavorits(ObservableList value) { + _$restaurantsFavoritsAtom.reportWrite(value, super.restaurantsFavorits, () { + super.restaurantsFavorits = value; + }); + } + + @override + String toString() { + return ''' +restaurantsFavorits: ${restaurantsFavorits} + '''; + } +} diff --git a/lib/modules/home/ui/details_restaurant_page.dart b/lib/modules/home/ui/details_restaurant_page.dart new file mode 100644 index 00000000..ac9ead77 --- /dev/null +++ b/lib/modules/home/ui/details_restaurant_page.dart @@ -0,0 +1,244 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:restaurantour/core/utils/dependency_injector.dart'; +import 'package:restaurantour/modules/home/data/models/restaurant.dart'; +import 'package:restaurantour/modules/home/domain/controllers/details_restaurant_controller.dart'; +import 'package:restaurantour/modules/home/domain/stores/favorite_store.dart'; + +import '../../../core/theme/app_theme.dart'; +import '../../../core/theme/text_theme.dart'; +import '../../../core/utils/constants.dart'; +import 'widgets/circular_progress_widget.dart'; + +class DetailsRestaurantPage extends StatefulWidget { + final Restaurant restaurant; + const DetailsRestaurantPage({Key? key, required this.restaurant}) : super(key: key); + + @override + State createState() => _DetailsRestaurantPageState(); +} + +class _DetailsRestaurantPageState extends State { + final DetailsRestaurantController controller = locator.get(); + final FavoriteStore favoriteStore = locator.get(); + + @override + void initState() { + controller.loadRestaurant(widget.restaurant); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Observer( + builder: (context) { + return Text( + key: const Key(ConstantsApp.kTitleDetailsRestaurant), + controller.status.isLoading ? '' : widget.restaurant.name ?? '', + ); + }, + ), + actions: [ + Observer( + builder: (context) { + final bool isFavorite = favoriteStore.restaurantsFavorits + .firstWhereOrNull((element) => element.id == controller.restaurant.id) != + null; + return Offstage( + offstage: !controller.status.isSuccess, + child: IconButton( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + onPressed: () async { + final bool isFavorite = favoriteStore.restaurantsFavorits + .firstWhereOrNull((element) => element.id == controller.restaurant.id) != + null; + if (isFavorite) { + favoriteStore.removeFavorite(controller.restaurant); + } else { + favoriteStore.addFavorite(controller.restaurant); + } + }, + icon: Icon(isFavorite ? Icons.favorite : Icons.favorite_border), + ), + ); + }, + ), + ], + ), + body: Observer( + builder: (context) { + if (controller.status.isLoading) return const CircularProgressWidget(); + + if (controller.status.isFailure) { + return Center( + key: const Key(ConstantsApp.kErrorDetailsRestaurant), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const SizedBox(height: 100), + Icon(Icons.error_outline, size: 120, color: AppTheme.redColor.withOpacity(0.7)), + const SizedBox(height: 16), + const Text( + 'Error returning to restaurant,\nplease try again later.', + style: AppTextStyle.black14w600, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => controller.loadRestaurant(controller.restaurant), + child: const Text('REFRESH', style: AppTextStyle.black16w600), + ), + ], + ), + ), + ); + } + + return ListView( + children: [ + controller.restaurant.heroImage.isEmpty + ? Container( + color: Colors.grey.shade300, + height: 361, + width: double.maxFinite, + child: const Icon(Icons.image_not_supported_outlined, color: Colors.black54, size: 150), + ) + : Hero( + tag: controller.restaurant.heroImage, + child: CachedNetworkImage( + imageUrl: controller.restaurant.heroImage, + height: 361, + width: double.maxFinite, + fit: BoxFit.cover, + progressIndicatorBuilder: (context, url, progress) => const CircularProgressWidget(), + ), + ), + Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(controller.restaurant.displayCategory, style: AppTextStyle.black12w400OpenSans), + const Spacer(), + Text( + controller.restaurant.isOpen ? 'Open Now' : 'Closed', + style: AppTextStyle.black12w400OpenSans.copyWith(fontStyle: FontStyle.italic), + textAlign: TextAlign.right, + ), + Padding( + padding: const EdgeInsets.only(left: 8, bottom: 3), + child: CircleAvatar( + backgroundColor: controller.restaurant.isOpen ? AppTheme.greenColor : AppTheme.redColor, + radius: 4, + ), + ), + ], + ), + const Padding(padding: EdgeInsets.symmetric(vertical: 24), child: Divider()), + const Text('Address', style: AppTextStyle.black12w400OpenSans), + Padding( + padding: const EdgeInsets.only(top: 24), + child: Text( + controller.restaurant.location?.formattedAddress ?? '', + style: AppTextStyle.black14w600OpenSans, + ), + ), + const Padding(padding: EdgeInsets.symmetric(vertical: 24), child: Divider()), + const Text('Overall Rating', style: AppTextStyle.black12w400OpenSans), + Padding( + padding: const EdgeInsets.only(top: 16), + child: Row( + children: [ + Text( + '${controller.restaurant.rating ?? 0}', + style: AppTextStyle.black18w700.copyWith(fontSize: 28), + ), + const Padding( + padding: EdgeInsets.only(top: 8), + child: Icon(Icons.star, color: AppTheme.yellowColor, size: 16), + ), + ], + ), + ), + const Padding(padding: EdgeInsets.symmetric(vertical: 24), child: Divider()), + if ((controller.restaurant.reviews?.length ?? 0) > 0) ...[ + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text( + (controller.restaurant.reviews?.length ?? 0) == 1 + ? '1 Review' + : '${controller.restaurant.reviews?.length} Reviews', + style: AppTextStyle.black12w400OpenSans, + ), + ), + ListView.separated( + shrinkWrap: true, + itemCount: controller.restaurant.reviews!.length, + physics: const NeverScrollableScrollPhysics(), + separatorBuilder: (context, index) => const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Divider(), + ), + itemBuilder: (context, index) { + final review = controller.restaurant.reviews![index]; + return Column( + key: Key('${ConstantsApp.kReviewsRestaurant}_$index'), + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: List.generate( + review.rating?.round() ?? 0, + (index) => const Icon(Icons.star, color: AppTheme.yellowColor, size: 16), + ).toList(), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + review.text ?? '', + style: AppTextStyle.black14w400OpenSans.copyWith(fontSize: 16), + ), + ), + Row( + children: [ + Container( + height: 40, + width: 40, + clipBehavior: Clip.hardEdge, + decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.grey.shade300), + child: (review.user?.imageUrl ?? '').isEmpty + ? const Icon(Icons.image_not_supported_outlined, color: Colors.black54) + : CachedNetworkImage( + imageUrl: review.user?.imageUrl ?? '', + fit: BoxFit.cover, + progressIndicatorBuilder: (context, url, progress) => + const CircularProgressWidget(), + ), + ), + const SizedBox(width: 8), + Text(review.user?.name ?? '', style: AppTextStyle.black12w400OpenSans), + ], + ), + ], + ); + }, + ), + ], + ], + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/modules/home/ui/home_page.dart b/lib/modules/home/ui/home_page.dart new file mode 100644 index 00000000..0bd7f1c5 --- /dev/null +++ b/lib/modules/home/ui/home_page.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:restaurantour/core/theme/app_theme.dart'; +import 'package:restaurantour/core/theme/text_theme.dart'; +import 'package:restaurantour/core/utils/constants.dart'; +import 'package:restaurantour/core/utils/dependency_injector.dart'; +import 'package:restaurantour/modules/home/data/models/restaurant.dart'; +import 'package:restaurantour/modules/home/domain/controllers/home_controller.dart'; +import 'package:restaurantour/modules/home/ui/widgets/circular_progress_widget.dart'; + +import 'widgets/card_restaurant_widget.dart'; + +class HomePage extends StatefulWidget { + const HomePage({Key? key}) : super(key: key); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + final HomeController controller = locator.get(); + + @override + void initState() { + controller.loadingInfos(); + super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DefaultTabController( + length: 2, + child: Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: const Text('RestauranteTour'), + bottom: const PreferredSize( + preferredSize: Size.fromHeight(50), + child: TabBar(tabs: [Tab(text: 'All Restaurants'), Tab(text: 'My Favorites')]), + ), + ), + body: TabBarView( + children: [ + Observer( + builder: (context) { + if (controller.status.isLoading) return const CircularProgressWidget(); + + if (controller.status.isFailure) { + // * Error list of restaurants + return Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const SizedBox(height: 100), + Icon(Icons.error_outline, size: 120, color: AppTheme.redColor.withOpacity(0.7)), + const SizedBox(height: 16), + const Text( + 'Error returning restaurants,\nplease try again later.', + style: AppTextStyle.black14w600, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: controller.loadingInfos, + child: const Text('REFRESH', style: AppTextStyle.black16w600), + ), + ], + ), + ), + ); + } + + return CustomScrollView( + key: const Key(ConstantsApp.kCustomScrollHomePage), + controller: controller.scrollController, + slivers: [ + // * List Restaurants + SliverVisibility( + visible: controller.restaurants.isNotEmpty, + replacementSliver: SliverToBoxAdapter( + child: Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const SizedBox(height: 100), + Icon( + Icons.subtitles_off_outlined, + size: 150, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + const Text('No restaurants in the moment', style: AppTextStyle.black14w600), + ], + ), + ), + ), + ), + sliver: SliverPadding( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), + sliver: SliverList.separated( + itemCount: controller.restaurants.length, + separatorBuilder: (context, index) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final Restaurant restaurant = controller.restaurants[index]; + return CardRestaurantWidget( + key: Key('${ConstantsApp.kCardRestaurant}_$index'), + restaurant: restaurant, + ); + }, + ), + ), + ), + // * Loading More + SliverVisibility( + visible: controller.status.isLoadingMore, + sliver: const SliverToBoxAdapter( + child: Center(child: Padding(padding: EdgeInsets.all(16), child: CircularProgressWidget())), + ), + ), + ], + ); + }, + ), + Observer( + builder: (context) { + if (controller.status.isLoading) return const CircularProgressWidget(); + + return Visibility( + visible: controller.restaurantsFavorits.isNotEmpty, + // * Empty List + replacement: Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + const SizedBox(height: 100), + Icon( + Icons.subtitles_off_outlined, + size: 150, + color: Colors.grey.shade400, + ), + const SizedBox(height: 16), + const Text('No favorites in the moment', style: AppTextStyle.black14w600), + ], + ), + ), + ), + + // * List Favorites + child: ListView.separated( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 12), + itemCount: controller.restaurantsFavorits.length, + separatorBuilder: (context, index) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final Restaurant restaurant = controller.restaurantsFavorits[index]; + return CardRestaurantWidget(restaurant: restaurant); + }, + ), + ); + // * Loading More + }, + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/home/ui/widgets/card_restaurant_widget.dart b/lib/modules/home/ui/widgets/card_restaurant_widget.dart new file mode 100644 index 00000000..4858a6ff --- /dev/null +++ b/lib/modules/home/ui/widgets/card_restaurant_widget.dart @@ -0,0 +1,110 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:restaurantour/core/animations/animate_do_fades.dart'; +import 'package:restaurantour/modules/home/data/models/restaurant.dart'; +import 'package:restaurantour/modules/home/ui/details_restaurant_page.dart'; + +import '../../../../core/theme/app_theme.dart'; +import '../../../../core/theme/text_theme.dart'; +import 'circular_progress_widget.dart'; + +class CardRestaurantWidget extends StatelessWidget { + final Restaurant restaurant; + + const CardRestaurantWidget({Key? key, required this.restaurant}) : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => Navigator.of(context).push( + MaterialPageRoute(builder: (context) => DetailsRestaurantPage(restaurant: restaurant)), + ), + child: FadeInUp( + duration: const Duration(milliseconds: 350), + child: Container( + height: 104, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 5.0, + offset: Offset(0.0, 1.0), + ), + ], + ), + child: Row( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + clipBehavior: Clip.hardEdge, + child: restaurant.heroImage.isEmpty + ? Container( + color: Colors.grey.shade300, + height: 88, + width: 88, + child: const Icon(Icons.image_not_supported_outlined, color: Colors.black54, size: 50), + ) + : Hero( + tag: restaurant.heroImage, + child: CachedNetworkImage( + imageUrl: restaurant.heroImage, + height: 88, + width: 88, + fit: BoxFit.cover, + progressIndicatorBuilder: (context, url, progress) => const CircularProgressWidget(), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + restaurant.name ?? '', + style: AppTextStyle.black16w600.copyWith(fontWeight: FontWeight.w500), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + const Spacer(), + Text( + '${restaurant.price ?? ''} ${restaurant.displayCategory}', + style: AppTextStyle.black12w400OpenSans, + ), + const SizedBox(height: 4), + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + ...List.generate( + restaurant.rating?.round() ?? 0, + (index) => const Icon(Icons.star, color: AppTheme.yellowColor, size: 16), + ).toList(), + const Spacer(), + Text( + restaurant.isOpen ? 'Open Now' : 'Closed', + style: AppTextStyle.black12w400OpenSans.copyWith(fontStyle: FontStyle.italic), + textAlign: TextAlign.right, + ), + Padding( + padding: const EdgeInsets.only(left: 8, bottom: 3), + child: CircleAvatar( + backgroundColor: restaurant.isOpen ? AppTheme.greenColor : AppTheme.redColor, + radius: 4, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/home/ui/widgets/circular_progress_widget.dart b/lib/modules/home/ui/widgets/circular_progress_widget.dart new file mode 100644 index 00000000..d447e283 --- /dev/null +++ b/lib/modules/home/ui/widgets/circular_progress_widget.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; +import 'package:restaurantour/core/utils/constants.dart'; + +class CircularProgressWidget extends StatelessWidget { + const CircularProgressWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Center( + key: Key(ConstantsApp.kLoading), + child: CircularProgressIndicator(strokeWidth: 4.0), + ); + } +} 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..451e88ec 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "64.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.2.0" args: dependency: transitive description: name: args - sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" async: dependency: transitive description: @@ -45,10 +45,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 +61,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,34 +101,50 @@ packages: dependency: transitive description: name: built_value - sha256: b6c9911b2d670376918d5b8779bc27e0e612a94ec3ff0343689e991d8d0a3b8a + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.1.4" - characters: + version: "8.9.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + url: "https://pub.dev" + source: hosted + version: "3.3.1" + cached_network_image_platform_interface: dependency: transitive description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + name: cached_network_image_platform_interface + sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" url: "https://pub.dev" source: hosted - version: "1.3.0" - charcode: + version: "4.0.0" + cached_network_image_web: dependency: transitive description: - name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + name: cached_network_image_web + sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" 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: @@ -146,29 +162,29 @@ packages: source: hosted version: "4.10.0" collection: - dependency: transitive + dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.2" convert: dependency: transitive description: name: convert - sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" cupertino_icons: dependency: "direct main" description: @@ -181,10 +197,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.6" dio: dependency: "direct main" description: @@ -201,27 +217,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_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + url: "https://pub.dev" + source: hosted + version: "3.3.1" flutter_lints: dependency: "direct dev" description: @@ -230,35 +262,56 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + flutter_mobx: + dependency: "direct main" + description: + name: flutter_mobx + sha256: "859fbf452fa9c2519d2700b125dd7fb14c508bbdd7fb65e26ca8ff6c92280e2e" + url: "https://pub.dev" + source: hosted + version: "2.2.1+1" 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 source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: e6017ce7fdeaf218dc51a100344d8cb70134b80e28b760f8bb23c242437bafd7 + url: "https://pub.dev" + source: hosted + version: "7.6.7" glob: dependency: transitive description: name: glob - sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" graphs: dependency: transitive description: @@ -267,38 +320,46 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + http: + dependency: transitive + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: bfb651625e251a88804ad6d596af01ea903544757906addcb2dcdf088b5ea185 + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" io: dependency: transitive description: name: io - sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174 + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.1" json_annotation: dependency: "direct main" description: @@ -327,10 +388,10 @@ packages: dependency: transitive description: name: logging - sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" matcher: dependency: transitive description: @@ -351,26 +412,66 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.9.1" mime: dependency: transitive description: name: mime - sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" + mobx: + dependency: "direct main" + description: + name: mobx + sha256: "63920b27b32ad1910adfe767ab1750e4c212e8923232a1f891597b362074ea5e" + url: "https://pub.dev" + source: hosted + version: "2.3.3+2" + mobx_codegen: + dependency: "direct dev" + description: + name: mobx_codegen + sha256: "8e0d8653a0c720ad933cd8358f6f89f740ce89203657c13f25bea772ef1fff7c" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + 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" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + url: "https://pub.dev" + source: hosted + version: "2.0.0" package_config: dependency: transitive description: name: package_config - sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: transitive description: @@ -387,54 +488,190 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "51f0d2c554cfbc9d6a312ab35152fc77e2f0b758ce9f1a444a3a1e5b8f3c6b7f" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" petitparser: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "5.4.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: name: pool - sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" pub_semver: dependency: transitive description: name: pub_semver - sha256: b5a5fcc6425ea43704852ba4453ba94b08c2226c63418a260240c3a054579014 + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: "3686efe4a4613a4449b1a4ae08670aadbd3376f2e78d93e3f8f0919db02a7256" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.3" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf + url: "https://pub.dev" + source: hosted + version: "2.2.1" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" shelf: dependency: transitive description: name: shelf - sha256: c240984c924796e055e831a0a36db23be8cb04f170b26df572931ab36418421d + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: fd84910bf7d58db109082edf7326b75322b8f186162028482f53dc892f00332d + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -464,30 +701,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + url: "https://pub.dev" + source: hosted + version: "2.3.2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: @@ -496,6 +757,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -508,50 +777,58 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.0" timing: dependency: transitive description: 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" + uuid: + dependency: transitive + description: + name: uuid + sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f" + url: "https://pub.dev" + source: hosted + version: "4.2.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: @@ -564,42 +841,58 @@ packages: dependency: transitive description: name: watcher - sha256: e42dfcc48f67618344da967b10f62de57e04bae01d9d3af4c2596f3712a88c99 + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" web: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.4.0" + win32: + dependency: transitive + description: + name: win32 + sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" xml: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.3.0" yaml: dependency: transitive description: name: yaml - sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.7.0-0" + dart: ">=3.1.0 <4.0.0" + flutter: ">=3.13.0" diff --git a/pubspec.yaml b/pubspec.yaml index be3055e0..b2193be8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,30 +1,64 @@ 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" + sdk: ">=2.17.0 <3.0.0" dependencies: + cached_network_image: ^3.3.1 + collection: ^1.17.2 + cupertino_icons: ^1.0.6 + dio: 5.4.0 flutter: sdk: flutter - cupertino_icons: ^1.0.6 - dio: ^5.4.0 - json_annotation: ^4.8.1 + flutter_mobx: ^2.2.1+1 flutter_svg: ^2.0.9 + get_it: ^7.6.7 + json_annotation: ^4.8.1 + mobx: ^2.3.3+2 + shared_preferences: ^2.2.3 dev_dependencies: + build_runner: ^2.4.9 + flutter_lints: ^1.0.2 flutter_test: sdk: flutter - flutter_lints: ^1.0.2 - build_runner: ^2.4.8 json_serializable: ^6.7.1 + mobx_codegen: ^2.6.1 + mocktail: ^1.0.3 flutter: uses-material-design: true -# assets: -# - assets/svg/ \ No newline at end of file + # assets: + # - assets/svg/ + fonts: + - family: Lora + fonts: + - asset: assets/fonts/Lora/static/Lora-Bold.ttf + - asset: assets/fonts/Lora/static/Lora-BoldItalic.ttf + - asset: assets/fonts/Lora/static/Lora-Italic.ttf + - asset: assets/fonts/Lora/static/Lora-Medium.ttf + - asset: assets/fonts/Lora/static/Lora-MediumItalic.ttf + - asset: assets/fonts/Lora/static/Lora-Regular.ttf + - asset: assets/fonts/Lora/static/Lora-SemiBold.ttf + - asset: assets/fonts/Lora/static/Lora-SemiBoldItalic.ttf + - asset: assets/fonts/Lora/Lora-Italic-VariableFont_wght.ttf + - asset: assets/fonts/Lora/Lora-VariableFont_wght.ttf + - family: Open Sans + fonts: + - asset: assets/fonts/Open_Sans/static/OpenSans-Bold.ttf + - asset: assets/fonts/Open_Sans/static/OpenSans-BoldItalic.ttf + - asset: assets/fonts/Open_Sans/static/OpenSans-ExtraBold.ttf + - asset: assets/fonts/Open_Sans/static/OpenSans-ExtraBoldItalic.ttf + - asset: assets/fonts/Open_Sans/static/OpenSans-Italic.ttf + - asset: assets/fonts/Open_Sans/static/OpenSans-Light.ttf + - asset: assets/fonts/Open_Sans/static/OpenSans-LightItalic.ttf + - asset: assets/fonts/Open_Sans/static/OpenSans-Medium.ttf + - asset: assets/fonts/Open_Sans/static/OpenSans-MediumItalic.ttf + - asset: assets/fonts/Open_Sans/static/OpenSans-Regular.ttf + - asset: assets/fonts/Open_Sans/static/OpenSans-SemiBold.ttf + - asset: assets/fonts/Open_Sans/static/OpenSans-SemiBoldItalic.ttf diff --git a/test/mocks/class_mocks.dart b/test/mocks/class_mocks.dart new file mode 100644 index 00000000..ab24dc90 --- /dev/null +++ b/test/mocks/class_mocks.dart @@ -0,0 +1,13 @@ +import 'package:dio/dio.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurantour/core/services/local_storage/local_storage_interface.dart'; +import 'package:restaurantour/modules/home/domain/repositories/restaurant_repository_interface.dart'; +import 'package:restaurantour/modules/home/domain/stores/favorite_store.dart'; + +class DioMock extends Mock implements Dio {} + +class FavoriteStoreMock extends Mock implements FavoriteStore {} + +class ResturantRepositoryMock extends Mock implements IRestaurantRepository {} + +class LocalStorageServiceMock extends Mock implements ILocalStorageService {} diff --git a/test/modules/home/data/repositories/yelp_repository_test.dart b/test/modules/home/data/repositories/yelp_repository_test.dart new file mode 100644 index 00000000..e1259d2b --- /dev/null +++ b/test/modules/home/data/repositories/yelp_repository_test.dart @@ -0,0 +1,225 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurantour/modules/home/data/models/restaurant.dart'; +import 'package:restaurantour/modules/home/data/repositories/yelp_repository.dart'; +import 'package:restaurantour/modules/home/domain/errors/erros.dart'; +import 'package:restaurantour/modules/home/domain/repositories/restaurant_repository_interface.dart'; + +import '../../../../mocks/class_mocks.dart'; + +void main() { + late Dio dioMock; + late IRestaurantRepository restaurantRepository; + + setUp(() { + dioMock = DioMock(); + restaurantRepository = YelpRepository(dio: dioMock); + }); + group('[GET RESTAURANTS]', () { + test('should returns a list of restaurants', () async { + //Arrange + const Restaurant restaurantMock = Restaurant(id: 'id_test'); + when(() => dioMock.post>(any(), data: any(named: 'data'))).thenAnswer( + (_) async => Response( + requestOptions: RequestOptions(), + statusCode: 200, + data: { + 'data': { + 'search': { + 'business': [restaurantMock.toJson()], + }, + }, + }, + ), + ); + + //Act + final response = await restaurantRepository.getRestaurants(); + + //Assert + expect(response.isSuccess, true); + expect(response.data!.isNotEmpty, true); + expect(response.data!.length, 1); + expect(response.data!.first.id, restaurantMock.id); + verify(() => dioMock.post>(any(), data: any(named: 'data'))).called(1); + }); + + test('should returns a empty list of restaurants', () async { + //Arrange + when(() => dioMock.post>(any(), data: any(named: 'data'))).thenAnswer( + (_) async => Response( + requestOptions: RequestOptions(), + statusCode: 200, + data: { + 'data': { + 'search': { + 'business': [], + }, + }, + }, + ), + ); + + //Act + final response = await restaurantRepository.getRestaurants(); + + //Assert + expect(response.isSuccess, true); + expect(response.data!.isEmpty, true); + verify(() => dioMock.post>(any(), data: any(named: 'data'))).called(1); + }); + + test('should returns a empty list of restaurants when you reach the limit of requests in the api', () async { + //Arrange + when(() => dioMock.post>(any(), data: any(named: 'data'))).thenThrow( + DioException.badResponse( + statusCode: 403, + requestOptions: RequestOptions(), + response: Response(requestOptions: RequestOptions(), statusCode: 403), + ), + ); + + //Act + final response = await restaurantRepository.getRestaurants(); + + //Assert + expect(response.isSuccess, true); + expect(response.data!.isEmpty, true); + verify(() => dioMock.post>(any(), data: any(named: 'data'))).called(1); + }); + + test('should returns a exception when server has unexpected error', () async { + //Arrange + when(() => dioMock.post>(any(), data: any(named: 'data'))).thenThrow( + DioException.badResponse( + statusCode: 500, + requestOptions: RequestOptions(), + response: Response(requestOptions: RequestOptions(), statusCode: 500), + ), + ); + + //Act + final response = await restaurantRepository.getRestaurants(); + + //Assert + expect(response.isError, true); + verify(() => dioMock.post>(any(), data: any(named: 'data'))).called(1); + }); + + test('should returns a exception when has unexpected error in the method', () async { + //Arrange + when(() => dioMock.post>(any(), data: any(named: 'data'))) + .thenThrow(Exception('Unexpected error')); + + //Act + final response = await restaurantRepository.getRestaurants(); + + //Assert + expect(response.isError, true); + verify(() => dioMock.post>(any(), data: any(named: 'data'))).called(1); + }); + }); + + group('[GET RESTAURANT BY ID]', () { + test('should returns a restaurant', () async { + //Arrange + const Restaurant restaurantMock = Restaurant(id: 'id_test'); + when(() => dioMock.post>(any(), data: any(named: 'data'))).thenAnswer( + (_) async => Response( + requestOptions: RequestOptions(), + statusCode: 200, + data: { + 'data': { + 'business': restaurantMock.toJson(), + }, + }, + ), + ); + + //Act + final response = await restaurantRepository.getRestaurantById(id: restaurantMock.id!); + + //Assert + expect(response.isSuccess, true); + expect(response.data!.id, restaurantMock.id); + verify(() => dioMock.post>(any(), data: any(named: 'data'))).called(1); + }); + + test('should returns a not found exception when not found restaurant', () async { + //Arrange + const Restaurant restaurantMock = Restaurant(id: 'id_test'); + when(() => dioMock.post>(any(), data: any(named: 'data'))).thenAnswer( + (_) async => Response( + requestOptions: RequestOptions(), + statusCode: 200, + data: { + 'data': { + 'business': null, + }, + }, + ), + ); + + //Act + final response = await restaurantRepository.getRestaurantById(id: restaurantMock.id!); + + //Assert + expect(response.isError, true); + expect(response.error is NotFoundRestaurant, true); + verify(() => dioMock.post>(any(), data: any(named: 'data'))).called(1); + }); + + test('should returns a exception when you reach the limit of requests in the api', () async { + //Arrange + const Restaurant restaurantMock = Restaurant(id: 'id_test'); + when(() => dioMock.post>(any(), data: any(named: 'data'))).thenThrow( + DioException.badResponse( + statusCode: 403, + requestOptions: RequestOptions(), + response: Response(requestOptions: RequestOptions(), statusCode: 403), + ), + ); + + //Act + final response = await restaurantRepository.getRestaurantById(id: restaurantMock.id!); + + //Assert + expect(response.isError, true); + verify(() => dioMock.post>(any(), data: any(named: 'data'))).called(1); + }); + + test('should returns a exception when server has unexpected error', () async { + //Arrange + const Restaurant restaurantMock = Restaurant(id: 'id_test'); + when(() => dioMock.post>(any(), data: any(named: 'data'))).thenThrow( + DioException.badResponse( + statusCode: 500, + requestOptions: RequestOptions(), + response: Response(requestOptions: RequestOptions(), statusCode: 500), + ), + ); + + //Act + final response = await restaurantRepository.getRestaurantById(id: restaurantMock.id!); + + //Assert + expect(response.isError, true); + verify(() => dioMock.post>(any(), data: any(named: 'data'))).called(1); + }); + + test('should returns a exception when has unexpected error in the method', () async { + //Arrange + const Restaurant restaurantMock = Restaurant(id: 'id_test'); + when(() => dioMock.post>(any(), data: any(named: 'data'))) + .thenThrow(Exception('Unexpected error')); + + //Act + final response = await restaurantRepository.getRestaurantById(id: restaurantMock.id!); + + //Assert + expect(response.isError, true); + verify(() => dioMock.post>(any(), data: any(named: 'data'))).called(1); + }); + }); +} diff --git a/test/modules/home/domain/controllers/details_restaurant_controller_test.dart b/test/modules/home/domain/controllers/details_restaurant_controller_test.dart new file mode 100644 index 00000000..60ea284c --- /dev/null +++ b/test/modules/home/domain/controllers/details_restaurant_controller_test.dart @@ -0,0 +1,52 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurantour/core/enums/status_type_enum.dart'; +import 'package:restaurantour/modules/home/data/models/response_model.dart'; +import 'package:restaurantour/modules/home/data/models/restaurant.dart'; +import 'package:restaurantour/modules/home/domain/controllers/details_restaurant_controller.dart'; +import 'package:restaurantour/modules/home/domain/errors/erros.dart'; +import 'package:restaurantour/modules/home/domain/repositories/restaurant_repository_interface.dart'; + +import '../../../../mocks/class_mocks.dart'; + +void main() { + late IRestaurantRepository restaurantRepository; + late DetailsRestaurantController detailsRestaurantController; + + setUp(() { + restaurantRepository = ResturantRepositoryMock(); + detailsRestaurantController = DetailsRestaurantController(restaurantRepository); + }); + + group('[GET RESTAURANT BY ID]', () { + test('should return a restaurant', () async { + //Arrange + const Restaurant restaurantMock = Restaurant(id: 'test_id', name: 'Test Name'); + when(() => restaurantRepository.getRestaurantById(id: any(named: 'id'))) + .thenAnswer((_) => Future.value(ResponseModel(data: restaurantMock))); + + //Act + await detailsRestaurantController.loadRestaurant(Restaurant(id: restaurantMock.id)); + + //Assert + expect(detailsRestaurantController.status, StatusType.success); + expect(detailsRestaurantController.restaurant.name!.isNotEmpty, true); + expect(detailsRestaurantController.restaurant.name, restaurantMock.name); + verify(() => restaurantRepository.getRestaurantById(id: any(named: 'id'))).called(1); + }); + + test('should return a exception when was not found restaurant', () async { + //Arrange + const Restaurant restaurantMock = Restaurant(id: 'test_id', name: 'Test Name'); + when(() => restaurantRepository.getRestaurantById(id: any(named: 'id'))) + .thenAnswer((_) => Future.value(ResponseModel(error: NotFoundRestaurant()))); + + //Act + await detailsRestaurantController.loadRestaurant(Restaurant(id: restaurantMock.id)); + + //Assert + expect(detailsRestaurantController.status, StatusType.failure); + verify(() => restaurantRepository.getRestaurantById(id: any(named: 'id'))).called(1); + }); + }); +} diff --git a/test/modules/home/ui/details_restaurant_page_test.dart b/test/modules/home/ui/details_restaurant_page_test.dart new file mode 100644 index 00000000..120530ba --- /dev/null +++ b/test/modules/home/ui/details_restaurant_page_test.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobx/mobx.dart' as mobx; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurantour/core/utils/constants.dart'; +import 'package:restaurantour/core/utils/dependency_injector.dart'; +import 'package:restaurantour/modules/home/data/models/response_model.dart'; +import 'package:restaurantour/modules/home/data/models/restaurant.dart'; +import 'package:restaurantour/modules/home/domain/controllers/details_restaurant_controller.dart'; +import 'package:restaurantour/modules/home/domain/repositories/restaurant_repository_interface.dart'; +import 'package:restaurantour/modules/home/domain/stores/favorite_store.dart'; +import 'package:restaurantour/modules/home/ui/details_restaurant_page.dart'; + +import '../../../mocks/class_mocks.dart'; + +void main() { + setUp(() { + locator.registerLazySingleton(() => FavoriteStoreMock()); + locator.registerSingleton(ResturantRepositoryMock()); + locator.registerFactory( + () => DetailsRestaurantController(locator.get()), + ); + }); + + tearDown(() => locator.reset()); + + testWidgets('should show details of restaurant', (WidgetTester tester) async { + //Arrange + const Restaurant restaurantFake = Restaurant( + id: 'test_id', + name: 'Test Name', + reviews: [Review(text: 'Review test 1')], + ); + when(() => locator.get().restaurantsFavorits).thenReturn(mobx.ObservableList()); + when(() => locator.get().getRestaurantById(id: any(named: 'id'))) + .thenAnswer((_) => Future.value(ResponseModel(data: restaurantFake))); + + //Act + await tester.pumpWidget(const MaterialApp(home: DetailsRestaurantPage(restaurant: restaurantFake))); + final finderTextTitle = find.byKey(const Key(ConstantsApp.kTitleDetailsRestaurant)); + final widgetBefore = tester.widget(finderTextTitle); + expect(finderTextTitle, findsOneWidget); + expect(widgetBefore.data?.isEmpty, true); + expect(find.byKey(const Key(ConstantsApp.kLoading)), findsOneWidget); + await tester.pumpAndSettle(); + + //Assert + expect(find.byKey(const Key(ConstantsApp.kErrorDetailsRestaurant)), findsNothing); + expect(finderTextTitle, findsOneWidget); + final widgetAfter = tester.widget(finderTextTitle); + expect(find.byKey(const Key('${ConstantsApp.kReviewsRestaurant}_0')), findsOneWidget); + expect(widgetAfter.data, restaurantFake.name); + }); + + testWidgets('should show widget of error when was not found restaurant', (WidgetTester tester) async { + //Arrange + when(() => locator.get().restaurantsFavorits).thenReturn(mobx.ObservableList()); + + //Act + await tester.pumpWidget(const MaterialApp(home: DetailsRestaurantPage(restaurant: Restaurant()))); + + await tester.pumpAndSettle(); + + //Assert + expect(find.byKey(const Key(ConstantsApp.kErrorDetailsRestaurant)), findsOneWidget); + }); +} diff --git a/test/modules/home/ui/home_page_test.dart b/test/modules/home/ui/home_page_test.dart new file mode 100644 index 00000000..c8faef4b --- /dev/null +++ b/test/modules/home/ui/home_page_test.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mobx/mobx.dart' as mobx; +import 'package:mocktail/mocktail.dart'; +import 'package:restaurantour/core/utils/constants.dart'; +import 'package:restaurantour/core/utils/dependency_injector.dart'; +import 'package:restaurantour/main.dart'; +import 'package:restaurantour/modules/home/data/models/response_model.dart'; +import 'package:restaurantour/modules/home/data/models/restaurant.dart'; +import 'package:restaurantour/modules/home/domain/controllers/home_controller.dart'; +import 'package:restaurantour/modules/home/domain/repositories/restaurant_repository_interface.dart'; +import 'package:restaurantour/modules/home/domain/stores/favorite_store.dart'; + +import '../../../mocks/class_mocks.dart'; + +void main() { + setUp(() { + locator.registerLazySingleton(() => FavoriteStoreMock()); + locator.registerSingleton(ResturantRepositoryMock()); + locator.registerFactory( + () => HomeController(locator.get(), locator.get()), + ); + }); + + tearDown(() => locator.reset()); + + testWidgets('should returns a list of restaurants', (WidgetTester tester) async { + //Arrange + const Restaurant restaurantFake = Restaurant( + id: 'test_id', + name: 'Test Name', + reviews: [Review(text: 'Review test 1')], + ); + when(() => locator.get().getFavorites()).thenAnswer((_) => Future.value()); + when(() => locator.get().restaurantsFavorits).thenReturn(mobx.ObservableList()); + when(() => locator.get().getRestaurants(offset: any(named: 'offset'))) + .thenAnswer((_) => Future.value(ResponseModel(data: [restaurantFake]))); + + //Act + await tester.pumpWidget(const Restaurantour()); + expect(find.byKey(const Key(ConstantsApp.kLoading)), findsOneWidget); + await tester.pumpAndSettle(); + + //Assert + expect(find.text('RestauranteTour'), findsOneWidget); + expect(find.byKey(const Key(ConstantsApp.kCustomScrollHomePage)), findsOneWidget); + expect(find.byKey(const Key('${ConstantsApp.kCardRestaurant}_0')), findsOneWidget); + }); + + testWidgets('shlud returns a empty list of restaurants', (WidgetTester tester) async { + //Arrange + when(() => locator.get().getFavorites()).thenAnswer((_) => Future.value()); + when(() => locator.get().restaurantsFavorits).thenReturn(mobx.ObservableList()); + when(() => locator.get().getRestaurants(offset: any(named: 'offset'))) + .thenAnswer((_) => Future.value(ResponseModel(data: []))); + + //Act + await tester.pumpWidget(const Restaurantour()); + expect(find.byKey(const Key(ConstantsApp.kLoading)), findsOneWidget); + await tester.pumpAndSettle(); + + //Assert + expect(find.text('RestauranteTour'), findsOneWidget); + expect(find.byKey(const Key(ConstantsApp.kCustomScrollHomePage)), findsOneWidget); + expect(find.byKey(const Key('${ConstantsApp.kCardRestaurant}_0')), findsNothing); + expect(find.text('No restaurants in the moment'), findsOneWidget); + }); + + testWidgets('should returns a widget error', (WidgetTester tester) async { + //Arrange + when(() => locator.get().getFavorites()).thenAnswer((_) => Future.value()); + when(() => locator.get().restaurantsFavorits).thenReturn(mobx.ObservableList()); + when(() => locator.get().getRestaurants(offset: any(named: 'offset'))) + .thenAnswer((_) => Future.value(ResponseModel(error: Exception()))); + + //Act + await tester.pumpWidget(const Restaurantour()); + expect(find.byKey(const Key(ConstantsApp.kLoading)), findsOneWidget); + await tester.pumpAndSettle(); + + //Assert + expect(find.text('RestauranteTour'), findsOneWidget); + expect(find.byKey(const Key(ConstantsApp.kCustomScrollHomePage)), findsNothing); + expect(find.byKey(const Key('${ConstantsApp.kCardRestaurant}_0')), findsNothing); + expect(find.bySubtype(), findsOneWidget); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 83fbeae4..00000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,20 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:restaurantour/main.dart'; - -void main() { - testWidgets('Page loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const Restaurantour()); - - // Verify that tests will run - expect(find.text('Fetch Restaurants'), findsOneWidget); - }); -}