diff --git a/.github/workflows/appcenter_android.yml b/.github/workflows/appcenter_android.yml new file mode 100644 index 0000000..a10e764 --- /dev/null +++ b/.github/workflows/appcenter_android.yml @@ -0,0 +1,41 @@ +name: Build and push Android + +on: + push: + branches: + - master + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-java@v1 + with: + java-version: '12.x' + - name: Cache Flutter Dependencies + uses: actions/cache@v1 + with: + path: /opt/hostedtoolcache/flutter + key: ${{ runner.os }}-flutter + - uses: subosito/flutter-action@v1 + with: + channel: 'dev' # or: 'dev' or 'beta' + - name: Install dependencies + run: flutter pub get + working-directory: ecommers + - name: generate files + run: flutter packages pub run build_runner build --delete-conflicting-outputs + working-directory: ecommers + - name: Build Android app + run: flutter build apk --target-platform android-arm + working-directory: ecommers + - name: upload artefact to App Center + uses: wzieba/AppCenter-Github-Action@v1.0.0 + with: + appName: EPAM/Flutter-eCom-Android + token: ${{secrets.AM_android_appcenter}} + group: Testers + file: ecommers/build/app/outputs/apk/release/app-release.apk diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..84f8cd9 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,31 @@ +name: build and check Android + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - uses: actions/setup-java@v1 + with: + java-version: '12.x' + - name: Cache Flutter Dependencies + uses: actions/cache@v1 + with: + path: /opt/hostedtoolcache/flutter + key: ${{ runner.os }}-flutter + - uses: subosito/flutter-action@v1 + with: + channel: 'dev' # or: 'dev' or 'beta' + - name: Install dependencies + run: flutter pub get + working-directory: ecommers + - name: generate files + run: flutter packages pub run build_runner build --delete-conflicting-outputs + working-directory: ecommers + - name: Build Android app + run: flutter build apk --target-platform android-arm + working-directory: ecommers diff --git a/LICENSE b/LICENSE index 96ee493..c9a6960 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,201 @@ -MIT License - -Copyright (c) 2020 Eugeny Sampir - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 EPAM Systems, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ecommers/.vscode/launch.json b/ecommers/.vscode/launch.json new file mode 100644 index 0000000..3287bb6 --- /dev/null +++ b/ecommers/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Flutter", + "request": "launch", + "type": "dart" + } + ] +} \ No newline at end of file diff --git a/ecommers/README.md b/ecommers/README.md index cec9f1e..e12b13e 100644 --- a/ecommers/README.md +++ b/ecommers/README.md @@ -1,6 +1,6 @@ # ecommers -A new Flutter project. +For the first run execute terminal command: 'flutter packages pub run build_runner build' ## Getting Started diff --git a/ecommers/analysis_options.yaml b/ecommers/analysis_options.yaml new file mode 100644 index 0000000..3de453a --- /dev/null +++ b/ecommers/analysis_options.yaml @@ -0,0 +1,14 @@ +include: package:lint/analysis_options.yaml + +analyzer: + exclude: + - lib/generated/i18n.dart + - lib/**.g.dart + - lib/**.chopper.dart + errors: + todo: warning + +linter: + rules: + prefer_single_quotes: true + avoid_classes_with_only_static_members: false \ No newline at end of file diff --git a/ecommers/android/app/build.gradle b/ecommers/android/app/build.gradle index 609f8c7..84e9dee 100644 --- a/ecommers/android/app/build.gradle +++ b/ecommers/android/app/build.gradle @@ -39,7 +39,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.ecommers" - minSdkVersion 16 + minSdkVersion 18 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/ecommers/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/ecommers/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4..6aa0471 100644 Binary files a/ecommers/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/ecommers/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/ecommers/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/ecommers/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b7..916e7fd 100644 Binary files a/ecommers/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/ecommers/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/ecommers/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/ecommers/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d4391..af93e89 100644 Binary files a/ecommers/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/ecommers/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/ecommers/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/ecommers/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d..ba896bb 100644 Binary files a/ecommers/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/ecommers/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/ecommers/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/ecommers/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372e..aeecd54 100644 Binary files a/ecommers/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/ecommers/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/ecommers/assets/add.svg b/ecommers/assets/add.svg new file mode 100644 index 0000000..3e0c59d --- /dev/null +++ b/ecommers/assets/add.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ecommers/assets/all_order.svg b/ecommers/assets/all_order.svg new file mode 100644 index 0000000..e2175b1 --- /dev/null +++ b/ecommers/assets/all_order.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ecommers/assets/apparel.svg b/ecommers/assets/apparel.svg new file mode 100644 index 0000000..6854b38 --- /dev/null +++ b/ecommers/assets/apparel.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ecommers/assets/arrow_right.svg b/ecommers/assets/arrow_right.svg new file mode 100644 index 0000000..f344dae --- /dev/null +++ b/ecommers/assets/arrow_right.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ecommers/assets/beauty.svg b/ecommers/assets/beauty.svg new file mode 100644 index 0000000..6f9ff8c --- /dev/null +++ b/ecommers/assets/beauty.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ecommers/assets/bell.svg b/ecommers/assets/bell.svg new file mode 100644 index 0000000..ddda8a7 --- /dev/null +++ b/ecommers/assets/bell.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/check_form.svg b/ecommers/assets/check_form.svg new file mode 100644 index 0000000..8973bbd --- /dev/null +++ b/ecommers/assets/check_form.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ecommers/assets/close_icon.svg b/ecommers/assets/close_icon.svg new file mode 100644 index 0000000..3c72026 --- /dev/null +++ b/ecommers/assets/close_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/credit_card.png b/ecommers/assets/credit_card.png new file mode 100644 index 0000000..2e28be7 Binary files /dev/null and b/ecommers/assets/credit_card.png differ diff --git a/ecommers/assets/currency.svg b/ecommers/assets/currency.svg new file mode 100644 index 0000000..9d4d189 --- /dev/null +++ b/ecommers/assets/currency.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/data/login.json b/ecommers/assets/data/login.json new file mode 100644 index 0000000..3256b08 --- /dev/null +++ b/ecommers/assets/data/login.json @@ -0,0 +1,5 @@ +{ + "access_token": "someToken", + "refresh_token": "refreshToken", + "expiration_date": "2021-01-01 12:00:00" +} \ No newline at end of file diff --git a/ecommers/assets/discuss_issue.svg b/ecommers/assets/discuss_issue.svg new file mode 100644 index 0000000..69ebcf7 --- /dev/null +++ b/ecommers/assets/discuss_issue.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/dress_image_cotton.png b/ecommers/assets/dress_image_cotton.png new file mode 100644 index 0000000..74ada66 Binary files /dev/null and b/ecommers/assets/dress_image_cotton.png differ diff --git a/ecommers/assets/dress_image_cotton2.png b/ecommers/assets/dress_image_cotton2.png new file mode 100644 index 0000000..dd62add Binary files /dev/null and b/ecommers/assets/dress_image_cotton2.png differ diff --git a/ecommers/assets/dress_image_floral.png b/ecommers/assets/dress_image_floral.png new file mode 100644 index 0000000..5257ce6 Binary files /dev/null and b/ecommers/assets/dress_image_floral.png differ diff --git a/ecommers/assets/dress_image_floral2.png b/ecommers/assets/dress_image_floral2.png new file mode 100644 index 0000000..65772f8 Binary files /dev/null and b/ecommers/assets/dress_image_floral2.png differ diff --git a/ecommers/assets/dress_image_pattern.png b/ecommers/assets/dress_image_pattern.png new file mode 100644 index 0000000..d302a72 Binary files /dev/null and b/ecommers/assets/dress_image_pattern.png differ diff --git a/ecommers/assets/dress_image_pattern2.png b/ecommers/assets/dress_image_pattern2.png new file mode 100644 index 0000000..4972a8f Binary files /dev/null and b/ecommers/assets/dress_image_pattern2.png differ diff --git a/ecommers/assets/electronics.svg b/ecommers/assets/electronics.svg new file mode 100644 index 0000000..8591509 --- /dev/null +++ b/ecommers/assets/electronics.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ecommers/assets/finished.svg b/ecommers/assets/finished.svg new file mode 100644 index 0000000..383b32b --- /dev/null +++ b/ecommers/assets/finished.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/furniture.svg b/ecommers/assets/furniture.svg new file mode 100644 index 0000000..81d6491 --- /dev/null +++ b/ecommers/assets/furniture.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ecommers/assets/girl2_image.png b/ecommers/assets/girl2_image.png new file mode 100644 index 0000000..bef22b1 Binary files /dev/null and b/ecommers/assets/girl2_image.png differ diff --git a/ecommers/assets/girl3_image.png b/ecommers/assets/girl3_image.png new file mode 100644 index 0000000..4c8f0d2 Binary files /dev/null and b/ecommers/assets/girl3_image.png differ diff --git a/ecommers/assets/girl_image.png b/ecommers/assets/girl_image.png new file mode 100644 index 0000000..33ffd90 Binary files /dev/null and b/ecommers/assets/girl_image.png differ diff --git a/ecommers/assets/green_backpack.png b/ecommers/assets/green_backpack.png new file mode 100644 index 0000000..4439c34 Binary files /dev/null and b/ecommers/assets/green_backpack.png differ diff --git a/ecommers/assets/home.svg b/ecommers/assets/home.svg new file mode 100644 index 0000000..750c6bf --- /dev/null +++ b/ecommers/assets/home.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ecommers/assets/invite_friends.svg b/ecommers/assets/invite_friends.svg new file mode 100644 index 0000000..724f1b1 --- /dev/null +++ b/ecommers/assets/invite_friends.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/language.svg b/ecommers/assets/language.svg new file mode 100644 index 0000000..ab6cbfd --- /dev/null +++ b/ecommers/assets/language.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ecommers/assets/launch_icon_android.png b/ecommers/assets/launch_icon_android.png new file mode 100644 index 0000000..123e692 Binary files /dev/null and b/ecommers/assets/launch_icon_android.png differ diff --git a/ecommers/assets/launch_icon_ios.png b/ecommers/assets/launch_icon_ios.png new file mode 100644 index 0000000..6ea49b0 Binary files /dev/null and b/ecommers/assets/launch_icon_ios.png differ diff --git a/ecommers/assets/mail_icon.svg b/ecommers/assets/mail_icon.svg new file mode 100644 index 0000000..a80db52 --- /dev/null +++ b/ecommers/assets/mail_icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/ecommers/assets/menu_arrow.svg b/ecommers/assets/menu_arrow.svg new file mode 100644 index 0000000..6b2b892 --- /dev/null +++ b/ecommers/assets/menu_arrow.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ecommers/assets/messages.svg b/ecommers/assets/messages.svg new file mode 100644 index 0000000..2124064 --- /dev/null +++ b/ecommers/assets/messages.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/ecommers/assets/notifications.svg b/ecommers/assets/notifications.svg new file mode 100644 index 0000000..28c8b7d --- /dev/null +++ b/ecommers/assets/notifications.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/password_icon.svg b/ecommers/assets/password_icon.svg new file mode 100644 index 0000000..71cc731 --- /dev/null +++ b/ecommers/assets/password_icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/ecommers/assets/payment.svg b/ecommers/assets/payment.svg new file mode 100644 index 0000000..aa95fd9 --- /dev/null +++ b/ecommers/assets/payment.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ecommers/assets/pending_payment.svg b/ecommers/assets/pending_payment.svg new file mode 100644 index 0000000..8208281 --- /dev/null +++ b/ecommers/assets/pending_payment.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/pending_shipments.svg b/ecommers/assets/pending_shipments.svg new file mode 100644 index 0000000..a3f90f7 --- /dev/null +++ b/ecommers/assets/pending_shipments.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/product_backpack.png b/ecommers/assets/product_backpack.png new file mode 100644 index 0000000..6e6cabb Binary files /dev/null and b/ecommers/assets/product_backpack.png differ diff --git a/ecommers/assets/product_scarf.png b/ecommers/assets/product_scarf.png new file mode 100644 index 0000000..1e188eb Binary files /dev/null and b/ecommers/assets/product_scarf.png differ diff --git a/ecommers/assets/product_shirt.png b/ecommers/assets/product_shirt.png new file mode 100644 index 0000000..9f24e01 Binary files /dev/null and b/ecommers/assets/product_shirt.png differ diff --git a/ecommers/assets/profile_icon.svg b/ecommers/assets/profile_icon.svg new file mode 100644 index 0000000..2972971 --- /dev/null +++ b/ecommers/assets/profile_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/progress.flr b/ecommers/assets/progress.flr new file mode 100644 index 0000000..a4868bc Binary files /dev/null and b/ecommers/assets/progress.flr differ diff --git a/ecommers/assets/rate_app.svg b/ecommers/assets/rate_app.svg new file mode 100644 index 0000000..fe04e27 --- /dev/null +++ b/ecommers/assets/rate_app.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/rate_star.svg b/ecommers/assets/rate_star.svg new file mode 100644 index 0000000..d1e71ab --- /dev/null +++ b/ecommers/assets/rate_star.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/remove.svg b/ecommers/assets/remove.svg new file mode 100644 index 0000000..97272a0 --- /dev/null +++ b/ecommers/assets/remove.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/right_icon.svg b/ecommers/assets/right_icon.svg new file mode 100644 index 0000000..a93c555 --- /dev/null +++ b/ecommers/assets/right_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/sale.png b/ecommers/assets/sale.png new file mode 100644 index 0000000..62d40b8 Binary files /dev/null and b/ecommers/assets/sale.png differ diff --git a/ecommers/assets/share_arrow.svg b/ecommers/assets/share_arrow.svg new file mode 100644 index 0000000..80df4af --- /dev/null +++ b/ecommers/assets/share_arrow.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ecommers/assets/shield.svg b/ecommers/assets/shield.svg new file mode 100644 index 0000000..2c5f960 --- /dev/null +++ b/ecommers/assets/shield.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ecommers/assets/shipping.svg b/ecommers/assets/shipping.svg new file mode 100644 index 0000000..9ebd701 --- /dev/null +++ b/ecommers/assets/shipping.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/ecommers/assets/shoes.svg b/ecommers/assets/shoes.svg new file mode 100644 index 0000000..35f6bf9 --- /dev/null +++ b/ecommers/assets/shoes.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ecommers/assets/stationary.svg b/ecommers/assets/stationary.svg new file mode 100644 index 0000000..e9801c3 --- /dev/null +++ b/ecommers/assets/stationary.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ecommers/assets/success.svg b/ecommers/assets/success.svg new file mode 100644 index 0000000..caf8315 --- /dev/null +++ b/ecommers/assets/success.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/assets/suggest.svg b/ecommers/assets/suggest.svg new file mode 100644 index 0000000..c02b853 --- /dev/null +++ b/ecommers/assets/suggest.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/ecommers/assets/support.svg b/ecommers/assets/support.svg new file mode 100644 index 0000000..c8ce54a --- /dev/null +++ b/ecommers/assets/support.svg @@ -0,0 +1,3 @@ + + + diff --git a/ecommers/i18n/en-US.json b/ecommers/i18n/en-US.json new file mode 100644 index 0000000..2519547 --- /dev/null +++ b/ecommers/i18n/en-US.json @@ -0,0 +1,40 @@ +{ + "ecommers": "ecommers", + "titleHomePage": "ecommers Home Page", + "homePageTitle": "Home page", + "categoriesTitle": "Categories", + "latetstTitle": "Latest", + "seeAllCategoryTitle": "See All", + "cartTitle": "Cart", + "totalOrder": "TOTAL", + "freeDomesticShipping": "Free Domestic Shipping", + "checkoutButton": "CHECKOUT", + "editProfile": "EDIT PROFILE", + "logOut": "LOG OUT", + "morePage": "More", + "checkoutTitle": "Checkout", + "placeOrderButton": "PLACE ORDER", + "addPromoCode": "Add Prome Code", + "items": "ITEMS", + "paymentMethod": "PAYMENT METHOD", + "shippingAddress": "SHIPPING ADDRESS", + "cardEnding": "Master Card ending", + "allCategories": "All Categories", + "signUp": "Sign Up", + "logIn": "Log In", + "forgotPassword": "Forgot Password", + "email": "EMAIL", + "username": "USERNAME", + "password": "PASSWORD", + "usernameOrEmail": "USERNAME / EMAIL", + "forgotPasswordHelpText": "Enter the email address you used to create your account and we will email you a link to reset your password", + "successMessage": "Your order was placed successfully. For more details, check All My Orders page under Profile tab", + "alertTitle": "Attention", + "alertLoginText": "Username or password is incorrect.", + "loginBottomTextSpan1": "Don’t have an account? Swipe right to \n", + "loginBottomTextSpan2": "create a new account.", + "signUpBottomTextSpan1": "By creating an account, you agree to our Privacy Policy \n", + "signUpBottomTextSpan2": "Terms of Service", + "signUpBottomTextSpan3": " and ", + "signUpBottomTextSpan4": "Privacy Policy" +} \ No newline at end of file diff --git a/ecommers/i18nconfig.json b/ecommers/i18nconfig.json new file mode 100644 index 0000000..515b2d7 --- /dev/null +++ b/ecommers/i18nconfig.json @@ -0,0 +1,12 @@ +{ + "defaultLocale": "en-US", + "locales": [ + "en-US" + ], + "localePath": "i18n", + "generatedPath": "lib/generated", + "ltr": [ + "en-US" + ], + "rtl": [] +} \ No newline at end of file diff --git a/ecommers/ios/Flutter/Debug.xcconfig b/ecommers/ios/Flutter/Debug.xcconfig index 592ceee..e8efba1 100644 --- a/ecommers/ios/Flutter/Debug.xcconfig +++ b/ecommers/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ecommers/ios/Flutter/Release.xcconfig b/ecommers/ios/Flutter/Release.xcconfig index 592ceee..399e934 100644 --- a/ecommers/ios/Flutter/Release.xcconfig +++ b/ecommers/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ecommers/ios/Podfile b/ecommers/ios/Podfile new file mode 100644 index 0000000..0ee4a53 --- /dev/null +++ b/ecommers/ios/Podfile @@ -0,0 +1,90 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.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 parse_KV_file(file, separator='=') + file_abs_path = File.expand_path(file) + if !File.exists? file_abs_path + return []; + end + generated_key_values = {} + skip_line_start_symbols = ["#", "/"] + File.foreach(file_abs_path) do |line| + next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } + plugin = line.split(pattern=separator) + if plugin.length == 2 + podname = plugin[0].strip() + path = plugin[1].strip() + podpath = File.expand_path("#{path}", file_abs_path) + generated_key_values[podname] = podpath + else + puts "Invalid plugin specification: #{line}" + end + end + generated_key_values +end + +target 'Runner' do + use_frameworks! + use_modular_headers! + + # Flutter Pod + + copied_flutter_dir = File.join(__dir__, 'Flutter') + copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') + copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') + unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) + # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. + # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. + # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. + + generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') + unless File.exist?(generated_xcode_build_settings_path) + raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) + cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; + + unless File.exist?(copied_framework_path) + FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) + end + unless File.exist?(copied_podspec_path) + FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) + end + end + + # Keep pod path relative so it can be checked into Podfile.lock. + pod 'Flutter', :path => 'Flutter' + + # Plugin Pods + + # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock + # referring to absolute paths on developers' machines. + system('rm -rf .symlinks') + system('mkdir -p .symlinks/plugins') + plugin_pods = parse_KV_file('../.flutter-plugins') + plugin_pods.each do |name, path| + symlink = File.join('.symlinks', 'plugins', name) + File.symlink(path, symlink) + pod name, :path => File.join(symlink, 'ios') + end +end + +# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. +install! 'cocoapods', :disable_input_output_paths => true + +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['ENABLE_BITCODE'] = 'NO' + end + end +end diff --git a/ecommers/ios/Runner.xcodeproj/project.pbxproj b/ecommers/ios/Runner.xcodeproj/project.pbxproj index 296f16e..670345f 100644 --- a/ecommers/ios/Runner.xcodeproj/project.pbxproj +++ b/ecommers/ios/Runner.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 535525C6E0F848FD27D5F8AB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DA8922DECF7128B1058B009 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -37,8 +38,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 = ""; }; + 38A211B064793AACD4E3792C /* 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 = ""; }; 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 50449812998ADB7CCC8BDDA8 /* 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 = ""; }; + 55DE18A60853724C21844B3A /* 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 = ""; }; + 5DA8922DECF7128B1058B009 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; @@ -59,12 +64,24 @@ files = ( 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + 535525C6E0F848FD27D5F8AB /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 7B63FD64ECAAC859288432BB /* Pods */ = { + isa = PBXGroup; + children = ( + 50449812998ADB7CCC8BDDA8 /* Pods-Runner.debug.xcconfig */, + 55DE18A60853724C21844B3A /* Pods-Runner.release.xcconfig */, + 38A211B064793AACD4E3792C /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -84,6 +101,8 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 7B63FD64ECAAC859288432BB /* Pods */, + D655B94E3F542BDBCD911664 /* Frameworks */, ); sourceTree = ""; }; @@ -118,6 +137,14 @@ name = "Supporting Files"; sourceTree = ""; }; + D655B94E3F542BDBCD911664 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5DA8922DECF7128B1058B009 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -125,12 +152,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + C3F9FD5412F4662F9DD13BA8 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + FB85BEE9BA8F07030175AA50 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -217,6 +246,43 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + C3F9FD5412F4662F9DD13BA8 /* [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; + }; + FB85BEE9BA8F07030175AA50 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + ); + 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/ecommers/ios/Runner.xcworkspace/contents.xcworkspacedata b/ecommers/ios/Runner.xcworkspace/contents.xcworkspacedata index 1d526a1..21a3cc1 100644 --- a/ecommers/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ecommers/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index dc9ada4..d0c1369 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 28c6bf0..861db08 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 2ccbfd9..e287b05 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index f091b6b..3847dfe 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cde121..787026b 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index d0ef06e..0e078dd 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index dcdc230..b5b997d 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 2ccbfd9..e287b05 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index c8f9ed8..874011d 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index a6d6b86..87d9f34 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..b4d6ff1 Binary files /dev/null and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..30de143 Binary files /dev/null and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..7b68462 Binary files /dev/null and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..a97df51 Binary files /dev/null and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index a6d6b86..87d9f34 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 75b2d16..4d84bed 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..f66199a Binary files /dev/null and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..cb21b5a Binary files /dev/null and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index c4df70d..495e2f4 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 6a84f41..f5c648d 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index d0e1f58..c606309 100644 Binary files a/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ecommers/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ecommers/lib/core/app_services/auth_response.dart b/ecommers/lib/core/app_services/auth_response.dart new file mode 100644 index 0000000..fa4720a --- /dev/null +++ b/ecommers/lib/core/app_services/auth_response.dart @@ -0,0 +1,6 @@ +class AuthResponse { + final bool isSuccessful; + final String error; + + AuthResponse({this.isSuccessful, this.error}); +} diff --git a/ecommers/lib/core/app_services/authorization_service.dart b/ecommers/lib/core/app_services/authorization_service.dart new file mode 100644 index 0000000..9c0808e --- /dev/null +++ b/ecommers/lib/core/app_services/authorization_service.dart @@ -0,0 +1,38 @@ +import 'package:chopper/chopper.dart'; +import 'package:ecommers/core/app_services/auth_response.dart'; +import 'package:ecommers/core/models/login_model.dart'; +import 'package:ecommers/core/models/user_model.dart'; +import 'package:ecommers/core/services/index.dart'; + +class AuthorizationService { + Future tryLogin(String username, String password) async { + final userJson = UserModel(username, null, password).toJson(); + + final Response response = await apiService.login(userJson); + + if (response.isSuccessful) { + membershipService.refresh(response.body); + } + + return response.isSuccessful; + } + + Future tryAuthorize( + String username, String email, String password) async { + final userJson = UserModel(username, email, password).toJson(); + final Response response = await apiService.auth(userJson); + + if (response.isSuccessful) { + membershipService.refresh(response.body); + + return AuthResponse(isSuccessful: response.isSuccessful); + } + + return AuthResponse( + isSuccessful: response.isSuccessful, error: response.bodyString); + } + + Future logOut() async { + await membershipService.clear(); + } +} diff --git a/ecommers/lib/core/app_services/index.dart b/ecommers/lib/core/app_services/index.dart new file mode 100644 index 0000000..6706775 --- /dev/null +++ b/ecommers/lib/core/app_services/index.dart @@ -0,0 +1 @@ +export 'authorization_service.dart'; diff --git a/ecommers/lib/core/common/api_defines.dart b/ecommers/lib/core/common/api_defines.dart new file mode 100644 index 0000000..674974a --- /dev/null +++ b/ecommers/lib/core/common/api_defines.dart @@ -0,0 +1,4 @@ +class ApiDefines { + static const String login = '/login'; + static const String auth = '/auth'; +} \ No newline at end of file diff --git a/ecommers/lib/core/common/categories.dart b/ecommers/lib/core/common/categories.dart new file mode 100644 index 0000000..07cb613 --- /dev/null +++ b/ecommers/lib/core/common/categories.dart @@ -0,0 +1,9 @@ +enum Categories { + apparel, + beauty, + electronics, + furniture, + home, + shoes, + stationary +} diff --git a/ecommers/lib/core/common/file_manager.dart b/ecommers/lib/core/common/file_manager.dart new file mode 100644 index 0000000..549d7f7 --- /dev/null +++ b/ecommers/lib/core/common/file_manager.dart @@ -0,0 +1,9 @@ +import 'package:flutter/services.dart'; + +class FileManager { + static const String jsonPath = 'assets/data/'; + + Future readJson(String fileName) async { + return rootBundle.loadString('$jsonPath$fileName'); + } +} \ No newline at end of file diff --git a/ecommers/lib/core/common/index.dart b/ecommers/lib/core/common/index.dart new file mode 100644 index 0000000..bdcd749 --- /dev/null +++ b/ecommers/lib/core/common/index.dart @@ -0,0 +1,5 @@ +export 'api_defines.dart'; +export 'categories.dart'; +export 'file_manager.dart'; +export 'json_serializable_converter.dart'; +export 'pages.dart'; diff --git a/ecommers/lib/core/common/json_serializable_converter.dart b/ecommers/lib/core/common/json_serializable_converter.dart new file mode 100644 index 0000000..a4aeef9 --- /dev/null +++ b/ecommers/lib/core/common/json_serializable_converter.dart @@ -0,0 +1,42 @@ +import 'package:chopper/chopper.dart'; + +typedef JsonFactory = T Function(Map json); + +class JsonSerializableConverter extends JsonConverter { + final Map _factories; + + const JsonSerializableConverter({Map factories}) + : _factories = factories; + + T _decodeMap(Map values) { + final jsonFactory = _factories[T]; + if (jsonFactory == null || jsonFactory is! JsonFactory) { + return throw 'not found factory'; + } + + return jsonFactory(values) as T; + } + + List _decodeList(List values) => + values.where((v) => v != null).map((v) => _decode(v) as T).toList(); + + dynamic _decode(entity) { + if (entity is Iterable) { + return _decodeList(entity as List); + } + + if (entity is Map) { + return _decodeMap(entity as Map); + } + + return entity; + } + + @override + Response convertResponse(Response response) { + final jsonRes = super.convertResponse(response); + + return jsonRes.copyWith( + body: _decode(jsonRes.body) as ResultType); + } +} diff --git a/ecommers/lib/core/common/pages.dart b/ecommers/lib/core/common/pages.dart new file mode 100644 index 0000000..bfb1744 --- /dev/null +++ b/ecommers/lib/core/common/pages.dart @@ -0,0 +1,12 @@ +enum Pages { + shell, + home, + search, + cart, + profile, + more, + categories, + authorization, + checkout, + success +} \ No newline at end of file diff --git a/ecommers/lib/core/models/auth_rich_text_span_model.dart b/ecommers/lib/core/models/auth_rich_text_span_model.dart new file mode 100644 index 0000000..4634297 --- /dev/null +++ b/ecommers/lib/core/models/auth_rich_text_span_model.dart @@ -0,0 +1,11 @@ +class AuthRichTextSpanModel { + final String text; + final bool isTappable; + final Function() onTap; + + AuthRichTextSpanModel({ + this.text = '', + this.isTappable = false, + this.onTap, + }); +} \ No newline at end of file diff --git a/ecommers/lib/core/models/index.dart b/ecommers/lib/core/models/index.dart new file mode 100644 index 0000000..647b372 --- /dev/null +++ b/ecommers/lib/core/models/index.dart @@ -0,0 +1,2 @@ +export 'auth_rich_text_span_model.dart'; +export 'order_model.dart'; diff --git a/ecommers/lib/core/models/login_model.dart b/ecommers/lib/core/models/login_model.dart new file mode 100644 index 0000000..7f2f458 --- /dev/null +++ b/ecommers/lib/core/models/login_model.dart @@ -0,0 +1,21 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'login_model.g.dart'; + +@JsonSerializable() +class LoginModel { + @JsonKey(name: 'access_token') + final String token; + + @JsonKey(name: 'refresh_token') + final String refreshToken; + + @JsonKey(name: 'expiration_date') + final String expirationDate; + + LoginModel(this.token, this.refreshToken, this.expirationDate); + + static const fromJsonFactory = _$LoginModelFromJson; + + Map toJson() => _$LoginModelToJson(this); +} diff --git a/ecommers/lib/core/models/order_model.dart b/ecommers/lib/core/models/order_model.dart new file mode 100644 index 0000000..5b49449 --- /dev/null +++ b/ecommers/lib/core/models/order_model.dart @@ -0,0 +1,15 @@ +class OrderModel { + final String title; + final String description; + final String imagePath; + final double cost; + int count; + + OrderModel({ + this.description, + this.cost, + this.imagePath, + this.title, + this.count, + }); +} diff --git a/ecommers/lib/core/models/user_model.dart b/ecommers/lib/core/models/user_model.dart new file mode 100644 index 0000000..5a685d9 --- /dev/null +++ b/ecommers/lib/core/models/user_model.dart @@ -0,0 +1,21 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'user_model.g.dart'; + +@JsonSerializable() +class UserModel { + @JsonKey(name: 'username') + final String username; + + @JsonKey(name: 'email') + final String email; + + @JsonKey(name: 'password') + final String password; + + UserModel(this.username, this.email, this.password); + + static const fromJsonFactory = _$UserModelFromJson; + + Map toJson() => _$UserModelToJson(this); +} \ No newline at end of file diff --git a/ecommers/lib/core/provider_models/index.dart b/ecommers/lib/core/provider_models/index.dart new file mode 100644 index 0000000..2207739 --- /dev/null +++ b/ecommers/lib/core/provider_models/index.dart @@ -0,0 +1 @@ +export 'shell_provider_model.dart'; diff --git a/ecommers/lib/core/provider_models/log_in_provider_model.dart b/ecommers/lib/core/provider_models/log_in_provider_model.dart new file mode 100644 index 0000000..8148f7f --- /dev/null +++ b/ecommers/lib/core/provider_models/log_in_provider_model.dart @@ -0,0 +1,45 @@ +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/core/models/index.dart'; +import 'package:ecommers/core/provider_models/provider_model_base.dart'; +import 'package:ecommers/core/services/index.dart'; +import 'package:ecommers/ui/utils/dialog_manager.dart'; +import 'package:flutter/material.dart'; + +class LogInProviderModel extends ProviderModelBase { + String username; + String password; + + List _bottomText; + List get bottomText => _getBottomText(); + + LogInProviderModel(BuildContext context) : super(context); + + Future tryLogin() async { + isBusy = true; + + final isSuccessful = + await authorizationService.tryLogin(username, password); + + isBusy = false; + + if (isSuccessful) { + await navigationService.navigateWithReplacementTo(Pages.shell); + } else { + await DialogManager.showAlertDialog( + context, localization.alertTitle, localization.alertLoginText); + } + } + + List _getBottomText() { + return _bottomText ??= [ + AuthRichTextSpanModel( + text: localization.loginBottomTextSpan1, + isTappable: false, + ), + AuthRichTextSpanModel( + text: localization.loginBottomTextSpan2, + isTappable: true, + ), + ]; + } +} diff --git a/ecommers/lib/core/provider_models/main_provider.dart b/ecommers/lib/core/provider_models/main_provider.dart new file mode 100644 index 0000000..5467f8a --- /dev/null +++ b/ecommers/lib/core/provider_models/main_provider.dart @@ -0,0 +1,13 @@ +import 'package:ecommers/core/provider_models/provider_model_base.dart'; +import 'package:ecommers/core/services/index.dart'; +import 'package:flutter/material.dart'; + +class MainProviderModel extends ProviderModelBase { + MainProviderModel(BuildContext context) : super(context); + + Future initialize() async { + isBusy = true; + await membershipService.load(); + isBusy = false; + } +} diff --git a/ecommers/lib/core/provider_models/provider_model_base.dart b/ecommers/lib/core/provider_models/provider_model_base.dart new file mode 100644 index 0000000..54660d4 --- /dev/null +++ b/ecommers/lib/core/provider_models/provider_model_base.dart @@ -0,0 +1,20 @@ +import 'package:ecommers/generated/i18n.dart'; +import 'package:flutter/material.dart'; + +abstract class ProviderModelBase extends ChangeNotifier { + @protected + final I18n localization; + + final BuildContext context; + + bool _isBusy = false; + + bool get isBusy => _isBusy; + + set isBusy(bool isBusy) { + _isBusy = isBusy; + notifyListeners(); + } + + ProviderModelBase(this.context) : localization = I18n.of(context); +} diff --git a/ecommers/lib/core/provider_models/shell_provider_model.dart b/ecommers/lib/core/provider_models/shell_provider_model.dart new file mode 100644 index 0000000..84ec4ee --- /dev/null +++ b/ecommers/lib/core/provider_models/shell_provider_model.dart @@ -0,0 +1,21 @@ +import 'package:ecommers/core/common/index.dart'; +import 'package:flutter/material.dart'; + +class ShellProviderModel with ChangeNotifier { + final List pages = [ + Pages.home, + Pages.search, + Pages.cart, + Pages.profile, + Pages.more + ]; + + int selectedItemIndex = 0; + + Pages get selectedPage => pages[selectedItemIndex]; + + void onTappedItem(int index) { + selectedItemIndex = index; + notifyListeners(); + } +} diff --git a/ecommers/lib/core/provider_models/sign_up_provider_model.dart b/ecommers/lib/core/provider_models/sign_up_provider_model.dart new file mode 100644 index 0000000..77a0a0e --- /dev/null +++ b/ecommers/lib/core/provider_models/sign_up_provider_model.dart @@ -0,0 +1,55 @@ +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/core/models/index.dart'; +import 'package:ecommers/core/provider_models/provider_model_base.dart'; +import 'package:ecommers/core/services/dependency_service.dart'; +import 'package:ecommers/ui/utils/dialog_manager.dart'; +import 'package:flutter/material.dart'; + +class SignUpProviderModel extends ProviderModelBase { + String email; + String username; + String password; + + List _bottomText; + + List get bottomText => _getBottomText(); + + SignUpProviderModel(BuildContext context) : super(context); + + List _getBottomText() { + return _bottomText ??= [ + AuthRichTextSpanModel( + text: localization.signUpBottomTextSpan1, + isTappable: false, + ), + AuthRichTextSpanModel( + text: localization.signUpBottomTextSpan2, + isTappable: true, + ), + AuthRichTextSpanModel( + text: localization.signUpBottomTextSpan3, + isTappable: false, + ), + AuthRichTextSpanModel( + text: localization.signUpBottomTextSpan4, + isTappable: true, + ), + ]; + } + + Future tryAuthorize() async { + isBusy = true; + + final authResponse = + await authorizationService.tryAuthorize(username, email, password); + + isBusy = false; + + if (authResponse.isSuccessful) { + await navigationService.navigateWithReplacementTo(Pages.shell); + } else { + await DialogManager.showAlertDialog( + context, localization.alertTitle, authResponse.error); + } + } +} diff --git a/ecommers/lib/core/services/api_service.dart b/ecommers/lib/core/services/api_service.dart new file mode 100644 index 0000000..2d1a231 --- /dev/null +++ b/ecommers/lib/core/services/api_service.dart @@ -0,0 +1,16 @@ +import 'package:chopper/chopper.dart'; +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/core/models/login_model.dart'; + +part 'api_service.chopper.dart'; + +@ChopperApi(baseUrl: '') +abstract class ApiService extends ChopperService { + static ApiService create([ChopperClient client]) => _$ApiService(client); + + @Post(path: ApiDefines.login) + Future> login(@Body() Map login); + + @Post(path: ApiDefines.auth) + Future> auth(@Body() Map auth); +} diff --git a/ecommers/lib/core/services/dependency_service.dart b/ecommers/lib/core/services/dependency_service.dart new file mode 100644 index 0000000..52b5199 --- /dev/null +++ b/ecommers/lib/core/services/dependency_service.dart @@ -0,0 +1,31 @@ +import 'package:ecommers/core/app_services/index.dart'; +import 'package:ecommers/core/common/file_manager.dart'; +import 'package:ecommers/core/services/api_service.dart'; +import 'package:ecommers/core/services/membership_service.dart'; +import 'package:ecommers/core/services/navigation/navigation_service.dart'; +import 'package:ecommers/web_server/request_handler.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:get_it/get_it.dart'; +import 'package:ecommers/core/services/extensions/get_it_extension.dart'; + +NavigationService get navigationService => GetIt.I.get(); +FileManager get fileManager => GetIt.I.get(); +ApiService get apiService => GetIt.I.get(); +MembershipService get membershipService => GetIt.I.get(); +RequestHandler get requestHandler => GetIt.I.get(); +AuthorizationService get authorizationService => GetIt.I.get(); + +class DependencyService { + static void registerDependencies() { + final GetIt serviceLocator = GetIt.instance; + + serviceLocator + ..registerLazySingleton(() => NavigationService()) + ..registerLazySingleton(() => FileManager()) + ..registerLazySingleton(() => RequestHandler()) + ..registerLazySingleton(() => AuthorizationService()) + ..registerLazySingleton( + () => MembershipService(const FlutterSecureStorage())) + ..registerHttpClient(); + } +} diff --git a/ecommers/lib/core/services/extensions/get_it_extension.dart b/ecommers/lib/core/services/extensions/get_it_extension.dart new file mode 100644 index 0000000..dad5c89 --- /dev/null +++ b/ecommers/lib/core/services/extensions/get_it_extension.dart @@ -0,0 +1,24 @@ +import 'package:chopper/chopper.dart'; +import 'package:ecommers/core/common/json_serializable_converter.dart'; +import 'package:ecommers/core/models/login_model.dart'; +import 'package:ecommers/core/services/api_service.dart'; +import 'package:ecommers/web_server/local_server.dart'; +import 'package:get_it/get_it.dart'; + +extension GetItExtension on GetIt { + void registerHttpClient() { + final chopper = ChopperClient( + baseUrl: LocalServer.uri.origin, + services: [ + ApiService.create(), + ], + converter: const JsonSerializableConverter( + factories: { + LoginModel: LoginModel.fromJsonFactory, + }, + ), + ); + + registerLazySingleton(() => ApiService.create(chopper)); + } +} diff --git a/ecommers/lib/core/services/index.dart b/ecommers/lib/core/services/index.dart new file mode 100644 index 0000000..1cae73a --- /dev/null +++ b/ecommers/lib/core/services/index.dart @@ -0,0 +1 @@ +export 'dependency_service.dart'; diff --git a/ecommers/lib/core/services/membership_service.dart b/ecommers/lib/core/services/membership_service.dart new file mode 100644 index 0000000..569ddad --- /dev/null +++ b/ecommers/lib/core/services/membership_service.dart @@ -0,0 +1,66 @@ +import 'package:ecommers/core/models/login_model.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class MembershipService { + static const String _accessTokenKey = 'accessTokenKey'; + static const String _refreshTokenKey = 'refreshTokenKey'; + static const String _expirationDateKey = 'expirationDateKey'; + + final FlutterSecureStorage secureStorage; + + String _accessToken; + String _refreshToken; + DateTime _expirationDate; + + String get accessToken => _accessToken; + String get refreshToken => _refreshToken; + bool get isNotExpired => + _expirationDate != null && DateTime.now().isBefore(_expirationDate); + + MembershipService(this.secureStorage); + + Future refresh(LoginModel loginModel) async { + _accessToken = loginModel.token; + _refreshToken = loginModel.refreshToken; + _expirationDate = DateTime.parse(loginModel.expirationDate); + + await _saveToStorage(loginModel); + } + + Future clear() async { + _accessToken = null; + _refreshToken = null; + _expirationDate = null; + + await _clearStorage(); + } + + Future _clearStorage() async { + await Future.wait( + { + secureStorage.delete(key: _accessTokenKey), + secureStorage.delete(key: _refreshTokenKey), + secureStorage.delete(key: _expirationDateKey), + }, + ); + } + + Future load() async { + _accessToken = await secureStorage.read(key: _accessTokenKey); + _refreshToken = await secureStorage.read(key: _refreshTokenKey); + _expirationDate = DateTime.tryParse( + await secureStorage.read(key: _expirationDateKey) ?? ''); + } + + Future _saveToStorage(LoginModel loginModel) async { + await Future.wait( + { + secureStorage.write(key: _accessTokenKey, value: loginModel.token), + secureStorage.write( + key: _refreshTokenKey, value: loginModel.refreshToken), + secureStorage.write( + key: _expirationDateKey, value: loginModel.expirationDate), + }, + ); + } +} diff --git a/ecommers/lib/core/services/navigation/navigation_service.dart b/ecommers/lib/core/services/navigation/navigation_service.dart new file mode 100644 index 0000000..cab0585 --- /dev/null +++ b/ecommers/lib/core/services/navigation/navigation_service.dart @@ -0,0 +1,54 @@ +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/ui/pages/authorization/index.dart'; +import 'package:ecommers/ui/pages/index.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class NavigationService { + final GlobalKey navigatorKey = GlobalKey(); + + Future navigateTo(Pages page, {Object arguments}) { + final route = _generateRoute(page, arguments); + return navigatorKey.currentState.push(route); + } + + Future navigateWithReplacementTo(Pages page, {Object arguments}) { + final route = _generateRoute(page, arguments); + return navigatorKey.currentState.pushReplacement(route); + } + + void goBack() { + navigatorKey.currentState.pop(); + } + + Route _generateRoute(Pages page, Object arguments) { + Widget resultPage; + + switch (page) { + case Pages.shell: + resultPage = ShellPage(); + break; + case Pages.categories: + resultPage = const CategoriesPage(); + break; + case Pages.authorization: + resultPage = const AuthorizationPage(); + break; + case Pages.checkout: + resultPage = CheckoutPage(); + break; + case Pages.success: + resultPage = const SuccessPage(); + break; + default: + resultPage = ShellPage(); + break; + } + + return _getRoute(resultPage); + } + + Route _getRoute(Widget widget) { + return CupertinoPageRoute(builder: (_) => widget); + } +} diff --git a/ecommers/lib/extensions/string_extension.dart b/ecommers/lib/extensions/string_extension.dart new file mode 100644 index 0000000..4d9682b --- /dev/null +++ b/ecommers/lib/extensions/string_extension.dart @@ -0,0 +1,3 @@ +extension StringExtension on String { + bool get isNullOrEmpty => this == null || isEmpty; +} \ No newline at end of file diff --git a/ecommers/lib/generated/i18n.dart b/ecommers/lib/generated/i18n.dart new file mode 100644 index 0000000..1347d6c --- /dev/null +++ b/ecommers/lib/generated/i18n.dart @@ -0,0 +1,167 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +// ignore_for_file: non_constant_identifier_names +// ignore_for_file: camel_case_types +// ignore_for_file: prefer_single_quotes +// ignore_for_file: unnecessary_brace_in_string_interps + +//WARNING: This file is automatically generated. DO NOT EDIT, all your changes would be lost. + +typedef LocaleChangeCallback = void Function(Locale locale); + +class I18n implements WidgetsLocalizations { + const I18n(); + static Locale _locale; + static bool _shouldReload = false; + + static set locale(Locale newLocale) { + _shouldReload = true; + I18n._locale = newLocale; + } + + static const GeneratedLocalizationsDelegate delegate = GeneratedLocalizationsDelegate(); + + /// function to be invoked when changing the language + static LocaleChangeCallback onLocaleChanged; + + static I18n of(BuildContext context) => + Localizations.of(context, WidgetsLocalizations); + + @override + TextDirection get textDirection => TextDirection.ltr; + + /// "ecommers" + String get ecommers => "ecommers"; + /// "ecommers Home Page" + String get titleHomePage => "ecommers Home Page"; + /// "Home page" + String get homePageTitle => "Home page"; + /// "Categories" + String get categoriesTitle => "Categories"; + /// "Latest" + String get latetstTitle => "Latest"; + /// "See All" + String get seeAllCategoryTitle => "See All"; + /// "Cart" + String get cartTitle => "Cart"; + /// "TOTAL" + String get totalOrder => "TOTAL"; + /// "Free Domestic Shipping" + String get freeDomesticShipping => "Free Domestic Shipping"; + /// "CHECKOUT" + String get checkoutButton => "CHECKOUT"; + /// "EDIT PROFILE" + String get editProfile => "EDIT PROFILE"; + /// "LOG OUT" + String get logOut => "LOG OUT"; + /// "More" + String get morePage => "More"; + /// "Checkout" + String get checkoutTitle => "Checkout"; + /// "PLACE ORDER" + String get placeOrderButton => "PLACE ORDER"; + /// "Add Prome Code" + String get addPromoCode => "Add Prome Code"; + /// "ITEMS" + String get items => "ITEMS"; + /// "PAYMENT METHOD" + String get paymentMethod => "PAYMENT METHOD"; + /// "SHIPPING ADDRESS" + String get shippingAddress => "SHIPPING ADDRESS"; + /// "Master Card ending" + String get cardEnding => "Master Card ending"; + /// "All Categories" + String get allCategories => "All Categories"; + /// "Sign Up" + String get signUp => "Sign Up"; + /// "Log In" + String get logIn => "Log In"; + /// "Forgot Password" + String get forgotPassword => "Forgot Password"; + /// "EMAIL" + String get email => "EMAIL"; + /// "USERNAME" + String get username => "USERNAME"; + /// "PASSWORD" + String get password => "PASSWORD"; + /// "USERNAME / EMAIL" + String get usernameOrEmail => "USERNAME / EMAIL"; + /// "Enter the email address you used to create your account and we will email you a link to reset your password" + String get forgotPasswordHelpText => "Enter the email address you used to create your account and we will email you a link to reset your password"; + /// "Your order was placed successfully. For more details, check All My Orders page under Profile tab" + String get successMessage => "Your order was placed successfully. For more details, check All My Orders page under Profile tab"; + /// "Attention" + String get alertTitle => "Attention"; + /// "Username or password is incorrect." + String get alertLoginText => "Username or password is incorrect."; + /// "Don’t have an account? Swipe right to \n" + String get loginBottomTextSpan1 => "Don’t have an account? Swipe right to \n"; + /// "create a new account." + String get loginBottomTextSpan2 => "create a new account."; + /// "By creating an account, you agree to our Privacy Policy \n" + String get signUpBottomTextSpan1 => "By creating an account, you agree to our Privacy Policy \n"; + /// "Terms of Service" + String get signUpBottomTextSpan2 => "Terms of Service"; + /// " and " + String get signUpBottomTextSpan3 => " and "; + /// "Privacy Policy" + String get signUpBottomTextSpan4 => "Privacy Policy"; +} + +class _I18n_en_US extends I18n { + const _I18n_en_US(); + + @override + TextDirection get textDirection => TextDirection.ltr; +} + +class GeneratedLocalizationsDelegate extends LocalizationsDelegate { + const GeneratedLocalizationsDelegate(); + List get supportedLocales { + return const [ + Locale("en", "US") + ]; + } + + LocaleResolutionCallback resolution({Locale fallback}) { + return (Locale locale, Iterable supported) { + if (isSupported(locale)) { + return locale; + } + final Locale fallbackLocale = fallback ?? supported.first; + return fallbackLocale; + }; + } + + @override + Future load(Locale locale) { + I18n._locale ??= locale; + I18n._shouldReload = false; + final String lang = I18n._locale != null ? I18n._locale.toString() : ""; + final String languageCode = I18n._locale != null ? I18n._locale.languageCode : ""; + if ("en_US" == lang) { + return SynchronousFuture(const _I18n_en_US()); + } + else if ("en" == languageCode) { + return SynchronousFuture(const _I18n_en_US()); + } + + return SynchronousFuture(const I18n()); + } + + @override + bool isSupported(Locale locale) { + for (var i = 0; i < supportedLocales.length && locale != null; i++) { + final l = supportedLocales[i]; + if (l.languageCode == locale.languageCode) { + return true; + } + } + return false; + } + + @override + bool shouldReload(GeneratedLocalizationsDelegate old) => I18n._shouldReload; +} \ No newline at end of file diff --git a/ecommers/lib/main.dart b/ecommers/lib/main.dart index 5a7af45..eb451a7 100644 --- a/ecommers/lib/main.dart +++ b/ecommers/lib/main.dart @@ -1,111 +1,80 @@ +import 'package:ecommers/core/provider_models/main_provider.dart'; +import 'package:ecommers/core/services/index.dart'; +import 'package:ecommers/generated/i18n.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/pages/authorization/authorization_page.dart'; +import 'package:ecommers/ui/pages/index.dart'; +import 'package:ecommers/ui/widgets/progress.dart'; +import 'package:ecommers/web_server/local_server.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; -void main() => runApp(MyApp()); +void main() { + runApp(MainApp()); + DependencyService.registerDependencies(); +} -class MyApp extends StatelessWidget { - // This widget is the root of your application. +class MainApp extends StatefulWidget { @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - // This is the theme of your application. - // - // Try running your application with "flutter run". You'll see the - // application has a blue toolbar. Then, without quitting the app, try - // changing the primarySwatch below to Colors.green and then invoke - // "hot reload" (press "r" in the console where you ran "flutter run", - // or simply save your changes to "hot reload" in a Flutter IDE). - // Notice that the counter didn't reset back to zero; the application - // is not restarted. - primarySwatch: Colors.blue, - ), - home: MyHomePage(title: 'Flutter Demo Home Page'), - ); - } + _MainAppState createState() => _MainAppState(); } -class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); - - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". +class _MainAppState extends State with WidgetsBindingObserver { + final GeneratedLocalizationsDelegate i18n = I18n.delegate; - final String title; + @override + void initState() { + WidgetsBinding.instance.addObserver(this); + LocalServer.setup(); + super.initState(); + } @override - _MyHomePageState createState() => _MyHomePageState(); -} + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } -class _MyHomePageState extends State { - int _counter = 0; + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + LocalServer.setup(); + } else if (state == AppLifecycleState.detached || + state == AppLifecycleState.inactive) { + LocalServer.closeConnection(); + } - void _incrementCounter() { - setState(() { - // This call to setState tells the Flutter framework that something has - // changed in this State, which causes it to rerun the build method below - // so that the display can reflect the updated values. If we changed - // _counter without calling setState(), then the build method would not be - // called again, and so nothing would appear to happen. - _counter++; - }); + super.didChangeAppLifecycleState(state); } @override Widget build(BuildContext context) { - // This method is rerun every time setState is called, for instance as done - // by the _incrementCounter method above. - // - // The Flutter framework has been optimized to make rerunning build methods - // fast, so that you can just rebuild anything that needs updating rather - // than having to individually change instances of widgets. - return Scaffold( - appBar: AppBar( - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: Text(widget.title), - ), - body: Center( - // Center is a layout widget. It takes a single child and positions it - // in the middle of the parent. - child: Column( - // Column is also a layout widget. It takes a list of children and - // arranges them vertically. By default, it sizes itself to fit its - // children horizontally, and tries to be as tall as its parent. - // - // Invoke "debug painting" (press "p" in the console, choose the - // "Toggle Debug Paint" action from the Flutter Inspector in Android - // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) - // to see the wireframe for each widget. - // - // Column has various properties to control how it sizes itself and - // how it positions its children. Here we use mainAxisAlignment to - // center the children vertically; the main axis here is the vertical - // axis because Columns are vertical (the cross axis would be - // horizontal). - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'You have pushed the button this many times:', - ), - Text( - '$_counter', - style: Theme.of(context).textTheme.display1, - ), - ], + return ChangeNotifierProvider( + create: (BuildContext context) => + MainProviderModel(context)..initialize(), + child: MaterialApp( + title: 'ecommers', + theme: ThemeProvider.getTheme(), + home: Consumer( + builder: (_, MainProviderModel provider, __) { + if (provider.isBusy) { + return Container( + color: BrandingColors.pageBackground, + child: const Center(child: Progress()), + ); + } + return membershipService.isNotExpired + ? ShellPage() + : const AuthorizationPage(); + }, + ), + navigatorKey: navigationService.navigatorKey, + localizationsDelegates: [i18n], + supportedLocales: i18n.supportedLocales, + localeResolutionCallback: i18n.resolution( + fallback: const Locale('en', 'US'), ), ), - floatingActionButton: FloatingActionButton( - onPressed: _incrementCounter, - tooltip: 'Increment', - child: Icon(Icons.add), - ), // This trailing comma makes auto-formatting nicer for build methods. ); } } diff --git a/ecommers/lib/ui/decorations/assets.dart b/ecommers/lib/ui/decorations/assets.dart new file mode 100644 index 0000000..b50383e --- /dev/null +++ b/ecommers/lib/ui/decorations/assets.dart @@ -0,0 +1,59 @@ +class Assets { + static const String apparelIcon = 'assets/apparel.svg'; + static const String beautyIcon = 'assets/beauty.svg'; + static const String electronicsIcon = 'assets/electronics.svg'; + static const String furnitureIcon = 'assets/furniture.svg'; + static const String homeIcon = 'assets/home.svg'; + static const String shoesIcon = 'assets/shoes.svg'; + static const String stationaryIcon = 'assets/stationary.svg'; + static const String arrowRightIcon = 'assets/arrow_right.svg'; + static const String shareArrowIcon = 'assets/share_arrow.svg'; + static const String rateStarIcon = 'assets/rate_star.svg'; + static const String allOrderIcon = 'assets/all_order.svg'; + static const String bellIcon = 'assets/bell.svg'; + static const String checkFormIcon = 'assets/check_form.svg'; + static const String currencyIcon = 'assets/currency.svg'; + static const String discussIssueIcon = 'assets/discuss_issue.svg'; + static const String finishedOrdersIcon = 'assets/finished.svg'; + static const String inviteFriendsIcon = 'assets/invite_friends.svg'; + static const String languageIcon = 'assets/language.svg'; + static const String paymentIcon = 'assets/payment.svg'; + static const String pendingPaymentIcon = 'assets/pending_payment.svg'; + static const String pendingShipmentIcon = 'assets/pending_shipments.svg'; + static const String rateAppIcon = 'assets/rate_app.svg'; + static const String shieldIcon = 'assets/shield.svg'; + static const String shippingIcon = 'assets/shipping.svg'; + static const String suggestIcon = 'assets/suggest.svg'; + static const String supportIcon = 'assets/support.svg'; + static const String menuArrowIcon = 'assets/menu_arrow.svg'; + static const String messagesIcon = 'assets/messages.svg'; + static const String notificationIcon = 'assets/notifications.svg'; + static const String arrowIcon = 'assets/right_icon.svg'; + static const String substractIcon = 'assets/remove.svg'; + static const String addIcon = 'assets/add.svg'; + static const String closeIcon = 'assets/close_icon.svg'; + static const String successIcon = 'assets/success.svg'; + static const String mailIcon = 'assets/mail_icon.svg'; + static const String profileIcon = 'assets/profile_icon.svg'; + static const String passwordIcon = 'assets/password_icon.svg'; + + static const String girlImage = 'assets/girl_image.png'; + static const String girl2Image = 'assets/girl2_image.png'; + static const String girl3Image = 'assets/girl3_image.png'; + static const String shirtImage = 'assets/product_shirt.png'; + static const String backpackImage = 'assets/product_backpack.png'; + static const String scarfImage = 'assets/product_scarf.png'; + static const String greenBackpackImage = 'assets/green_backpack.png'; + + static const String creditCardImage = 'assets/credit_card.png'; + static const String saleImage = 'assets/sale.png'; + + static const String dressCottonImage = 'assets/dress_image_cotton.png'; + static const String dressCotton2Image = 'assets/dress_image_cotton2.png'; + static const String dressFloralImage = 'assets/dress_image_floral.png'; + static const String dressFloral2Image = 'assets/dress_image_floral2.png'; + static const String dressPatternImage = 'assets/dress_image_pattern.png'; + static const String dressPattern2Image = 'assets/dress_image_pattern2.png'; + + static const String progressAnimation = 'assets/progress.flr'; +} diff --git a/ecommers/lib/ui/decorations/branding_colors.dart b/ecommers/lib/ui/decorations/branding_colors.dart new file mode 100644 index 0000000..27f1d77 --- /dev/null +++ b/ecommers/lib/ui/decorations/branding_colors.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class BrandingColors { + static const Color background = Color(0xffffffff); + static const Color pageBackground = Color(0xffF5F6F8); + static const Color blur = Color(0xffE7EAF0); + static const Color backgroundIcon = Color(0xffDBDEE2); + static const Color primaryText = Color(0xff515C6F); + static const Color secondaryText = Color(0xffffffff); + static const Color primary = Color(0xFFFF6969); + static const Color secondary = Color(0xFF727C8E); +} \ No newline at end of file diff --git a/ecommers/lib/ui/decorations/dimens/dimens.dart b/ecommers/lib/ui/decorations/dimens/dimens.dart new file mode 100644 index 0000000..b5c8262 --- /dev/null +++ b/ecommers/lib/ui/decorations/dimens/dimens.dart @@ -0,0 +1,11 @@ +import 'dart:ui'; + +import 'insets.dart'; + +class Dimens { + static const int defaultTextMaxLines = 1; + + static const Offset defaultBlurOffset = Offset(Insets.x0, Insets.x2); + + static const double pagePadding = Insets.x6; +} diff --git a/ecommers/lib/ui/decorations/dimens/font_sizes.dart b/ecommers/lib/ui/decorations/dimens/font_sizes.dart new file mode 100644 index 0000000..b88a653 --- /dev/null +++ b/ecommers/lib/ui/decorations/dimens/font_sizes.dart @@ -0,0 +1,10 @@ +class FontSizes { + static const small_1x = 10.0; + static const small_2x = 11.0; + static const small_3x = 12.0; + static const normal = 15.0; + static const big_1x = 18.0; + static const big_2x = 20.0; + static const big_3x = 22.0; + static const big_4x = 30.0; +} \ No newline at end of file diff --git a/ecommers/lib/ui/decorations/dimens/index.dart b/ecommers/lib/ui/decorations/dimens/index.dart new file mode 100644 index 0000000..e3fd207 --- /dev/null +++ b/ecommers/lib/ui/decorations/dimens/index.dart @@ -0,0 +1,4 @@ +export 'dimens.dart'; +export 'font_sizes.dart'; +export 'insets.dart'; +export 'radiuses.dart'; diff --git a/ecommers/lib/ui/decorations/dimens/insets.dart b/ecommers/lib/ui/decorations/dimens/insets.dart new file mode 100644 index 0000000..53de4b2 --- /dev/null +++ b/ecommers/lib/ui/decorations/dimens/insets.dart @@ -0,0 +1,20 @@ +class Insets { + static const x0 = 0.0; + static const x0_5 = 2.0; + static const x1 = 4.0; + static const x1_5 = 6.0; + static const x2 = 8.0; + static const x2_5 = 10.0; + static const x3 = 12.0; + static const x3_5 = 14.0; + static const x4 = 16.0; + static const x4_5 = 18.0; + static const x5 = 20.0; + static const x5_5 = 22.0; + static const x6 = 24.0; + static const x6_5 = 26.0; + static const x7 = 28.0; + static const x7_5 = 30.0; + static const x8 = 32.0; + static const x8_5 = 34.0; +} \ No newline at end of file diff --git a/ecommers/lib/ui/decorations/dimens/radiuses.dart b/ecommers/lib/ui/decorations/dimens/radiuses.dart new file mode 100644 index 0000000..3aa3e9f --- /dev/null +++ b/ecommers/lib/ui/decorations/dimens/radiuses.dart @@ -0,0 +1,7 @@ +class Radiuses { + static const small_1x = 5.0; + static const small_2x = 7.0; + static const normal = 10.0; + static const big_1x = 15.0; + static const big_2x = 24.0; +} \ No newline at end of file diff --git a/ecommers/lib/ui/decorations/gradients.dart b/ecommers/lib/ui/decorations/gradients.dart new file mode 100644 index 0000000..3422046 --- /dev/null +++ b/ecommers/lib/ui/decorations/gradients.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +class Gradients { + static Gradient get apparelCategory => + _createGradient(const Color(0xffFFAE4E), const Color(0xffFF7676)); + + static Gradient get beautyCategory => + _createGradient(const Color(0xff4EFFF8), const Color(0xff76BAFF)); + + static Gradient get shoesCategory => + _createGradient(const Color(0xffB4FF4E), const Color(0xff2FC145)); + + static Gradient get electronicsCategory => + _createGradient(const Color(0xffD5A3FF), const Color(0xff77A5F8)); + + static Gradient get furnitureCategory => + _createGradient(const Color(0xffFFF84E), const Color(0xffE6B15C)); + + static Gradient get homeCategory => + _createGradient(const Color(0xffFF74A4), const Color(0xff9F6EA3)); + + static Gradient get stationaryCategory => + _createGradient(const Color(0xff9D9E9F), const Color(0xff505862)); + + static Gradient get categoriesCompact => + _createGradient(const Color(0xffffffff), const Color(0xffffffff)); + + static LinearGradient _createGradient(Color beginColor, Color endColor) { + return LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [beginColor, endColor]); + } +} diff --git a/ecommers/lib/ui/decorations/index.dart b/ecommers/lib/ui/decorations/index.dart new file mode 100644 index 0000000..9619781 --- /dev/null +++ b/ecommers/lib/ui/decorations/index.dart @@ -0,0 +1,4 @@ +export 'assets.dart'; +export 'branding_colors.dart'; +export 'gradients.dart'; +export 'theme_provider.dart'; diff --git a/ecommers/lib/ui/decorations/theme_provider.dart b/ecommers/lib/ui/decorations/theme_provider.dart new file mode 100644 index 0000000..0761b39 --- /dev/null +++ b/ecommers/lib/ui/decorations/theme_provider.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'dimens/index.dart'; +import 'index.dart'; + +class ThemeProvider { + static ThemeData getTheme() { + return ThemeData( + primaryColor: BrandingColors.primary, + backgroundColor: BrandingColors.pageBackground, + cursorColor: BrandingColors.primary, + appBarTheme: const AppBarTheme( + brightness: Brightness.light, + color: BrandingColors.pageBackground, + elevation: 0.0, + ), + textTheme: TextTheme( + headline6: _TextStyles.headline6, + headline5: _TextStyles.headline5, + button: _TextStyles.button, + caption: _TextStyles.caption, + subtitle1: _TextStyles.subtitle1, + subtitle2: _TextStyles.subtitle2, + bodyText1: _TextStyles.bodyText1, + bodyText2: _TextStyles.bodyText2, + overline: _TextStyles.overline, + headline4: _TextStyles.headline4, + headline3: _TextStyles.headline3, + headline2: _TextStyles.headline2, + headline1: _TextStyles.headline1, + ), + ); + } +} + +class _TextStyles { + static const headline6 = TextStyle( + color: BrandingColors.primaryText, + fontSize: FontSizes.big_4x, + fontWeight: FontWeight.w700, + ); + + static final TextStyle headline5 = TextStyle( + color: BrandingColors.primaryText.withOpacity(0.5), + fontSize: FontSizes.small_3x, + fontWeight: FontWeight.w500, + ); + + static const button = TextStyle( + color: BrandingColors.secondary, + fontSize: FontSizes.small_3x, + fontWeight: FontWeight.w700, + ); + + static const caption = TextStyle( + color: BrandingColors.secondary, + fontSize: FontSizes.small_2x, + fontWeight: FontWeight.w400, + ); + + static const subtitle1 = TextStyle( + color: BrandingColors.primaryText, + fontSize: FontSizes.normal, + fontWeight: FontWeight.w400, + ); + + static const subtitle2 = TextStyle( + color: BrandingColors.primaryText, + fontSize: FontSizes.normal, + fontWeight: FontWeight.w300, + ); + + static const bodyText1 = TextStyle( + color: BrandingColors.primaryText, + fontSize: FontSizes.normal, + fontWeight: FontWeight.w500, + ); + + static const bodyText2 = TextStyle( + color: BrandingColors.primaryText, + fontSize: FontSizes.normal, + fontWeight: FontWeight.w300, + ); + + static const overline = TextStyle( + color: BrandingColors.secondaryText, + fontSize: FontSizes.small_1x, + fontWeight: FontWeight.w500, + ); + + static const headline4 = TextStyle( + color: BrandingColors.secondaryText, + fontSize: FontSizes.normal, + fontWeight: FontWeight.w400, + ); + + static const headline3 = TextStyle( + color: BrandingColors.secondaryText, + fontSize: FontSizes.big_1x, + fontWeight: FontWeight.w300, + ); + + static const headline2 = TextStyle( + color: BrandingColors.secondaryText, + fontSize: FontSizes.big_2x, + fontWeight: FontWeight.w400, + ); + + static const headline1 = TextStyle( + color: BrandingColors.secondaryText, + fontSize: FontSizes.big_3x, + fontWeight: FontWeight.w400, + ); +} diff --git a/ecommers/lib/ui/notifier_provider_widget.dart b/ecommers/lib/ui/notifier_provider_widget.dart new file mode 100644 index 0000000..f50990b --- /dev/null +++ b/ecommers/lib/ui/notifier_provider_widget.dart @@ -0,0 +1,47 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class NotifierProviderWidget extends StatefulWidget { + final Widget Function(BuildContext context, T model, Widget child) builder; + final T providerModel; + final Widget child; + final Function(T) onModelReady; + + const NotifierProviderWidget({ + Key key, + this.builder, + this.child, + this.providerModel, + this.onModelReady, + }) : super(key: key); + + @override + _NotifierProviderWidgetState createState() => _NotifierProviderWidgetState(); +} + +class _NotifierProviderWidgetState extends State> { + T providerModel; + + @override + void initState() { + providerModel = widget.providerModel; + + if (widget.onModelReady != null) { + widget.onModelReady(providerModel); + } + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => providerModel, + child: Consumer( + builder: widget.builder, + child: widget.child, + ), + ); + } +} \ No newline at end of file diff --git a/ecommers/lib/ui/pages/authorization/authentication_tab_base.dart b/ecommers/lib/ui/pages/authorization/authentication_tab_base.dart new file mode 100644 index 0000000..8ac5b39 --- /dev/null +++ b/ecommers/lib/ui/pages/authorization/authentication_tab_base.dart @@ -0,0 +1,26 @@ +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:flutter/material.dart'; + +class AuthorizationTabBase extends StatelessWidget { + final List children; + + const AuthorizationTabBase({this.children}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, viewportConstraints) { + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: Dimens.pagePadding), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: viewportConstraints.maxHeight), + child: IntrinsicHeight( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: children, + ), + ), + ), + ); + }); + } +} diff --git a/ecommers/lib/ui/pages/authorization/authorization_page.dart b/ecommers/lib/ui/pages/authorization/authorization_page.dart new file mode 100644 index 0000000..f8f1d8b --- /dev/null +++ b/ecommers/lib/ui/pages/authorization/authorization_page.dart @@ -0,0 +1,56 @@ +import 'package:ecommers/core/provider_models/log_in_provider_model.dart'; +import 'package:ecommers/core/provider_models/sign_up_provider_model.dart'; +import 'package:ecommers/generated/i18n.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/pages/authorization/forgot_password_page.dart'; +import 'package:ecommers/ui/pages/authorization/log_in_page.dart'; +import 'package:ecommers/ui/pages/authorization/sign_up_page.dart'; +import 'package:ecommers/ui/widgets/backgrounded_safe_area.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class AuthorizationPage extends StatelessWidget { + const AuthorizationPage({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final tabStyle = Theme.of(context).textTheme.headline6; + final localization = I18n.of(context); + + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => LogInProviderModel(context)), + ChangeNotifierProvider(create: (_) => SignUpProviderModel(context)), + ], + child: BackgroundedSafeArea( + child: DefaultTabController( + length: 3, + child: Scaffold( + backgroundColor: BrandingColors.pageBackground, + appBar: AppBar( + bottom: TabBar( + tabs: [ + Tab(text: localization.signUp), + Tab(text: localization.logIn), + Tab(text: localization.forgotPassword), + ], + indicatorColor: Colors.transparent, + labelStyle: tabStyle, + labelColor: tabStyle.color, + unselectedLabelColor: tabStyle.color.withOpacity(0.2), + isScrollable: true, + ), + ), + body: TabBarView( + children: [ + SignUpPage(), + LogInPage(), + ForgotPasswordPage(), + ], + ), + ), + ), + ), + ); + } +} diff --git a/ecommers/lib/ui/pages/authorization/forgot_password_page.dart b/ecommers/lib/ui/pages/authorization/forgot_password_page.dart new file mode 100644 index 0000000..ac6a41d --- /dev/null +++ b/ecommers/lib/ui/pages/authorization/forgot_password_page.dart @@ -0,0 +1,39 @@ +import 'package:ecommers/generated/i18n.dart'; +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/pages/authorization/authentication_tab_base.dart'; +import 'package:ecommers/ui/widgets/authorization/auth_text_field_area_container.dart'; +import 'package:ecommers/ui/widgets/authorization/index.dart'; +import 'package:ecommers/ui/widgets/button/index.dart'; +import 'package:flutter/material.dart'; + +class ForgotPasswordPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + final localization = I18n.of(context); + + return AuthorizationTabBase( + children: [ + Text( + localization.forgotPasswordHelpText, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyText2, + ), + const SizedBox(height: Insets.x8_5), + AuthTextFieldAreaContainer( + child: AuthTextField( + labelText: localization.email, + keyboardType: TextInputType.emailAddress, + assetIconPath: Assets.mailIcon, + ), + ), + const SizedBox(height: Insets.x3_5), + PrimaryButtonWidget( + text: localization.logIn, + assetIconPath: Assets.arrowRightIcon, + onPressedFunction: () {}, //TODO: add providers handler to it + ), + ], + ); + } +} diff --git a/ecommers/lib/ui/pages/authorization/index.dart b/ecommers/lib/ui/pages/authorization/index.dart new file mode 100644 index 0000000..4eef1ff --- /dev/null +++ b/ecommers/lib/ui/pages/authorization/index.dart @@ -0,0 +1,5 @@ +export 'authentication_tab_base.dart'; +export 'authorization_page.dart'; +export 'forgot_password_page.dart'; +export 'log_in_page.dart'; +export 'sign_up_page.dart'; diff --git a/ecommers/lib/ui/pages/authorization/log_in_page.dart b/ecommers/lib/ui/pages/authorization/log_in_page.dart new file mode 100644 index 0000000..ddebcac --- /dev/null +++ b/ecommers/lib/ui/pages/authorization/log_in_page.dart @@ -0,0 +1,70 @@ +import 'package:ecommers/core/provider_models/log_in_provider_model.dart'; +import 'package:ecommers/generated/i18n.dart'; +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/pages/authorization/index.dart'; +import 'package:ecommers/ui/widgets/authorization/index.dart'; +import 'package:ecommers/ui/widgets/button/index.dart'; +import 'package:ecommers/ui/widgets/progress.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class LogInPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Consumer( + builder: (_, LogInProviderModel provider, child) { + return Stack( + children: [ + child, + Visibility( + visible: provider.isBusy, + child: const Progress(), + ), + ], + ); + }, + child: _buildContent(context), + ); + } + + Widget _buildContent(BuildContext context) { + final localization = I18n.of(context); + final provider = Provider.of(context, listen: false); + + return AuthorizationTabBase( + children: [ + const SizedBox(height: Insets.x5), + AuthTextFieldAreaContainer( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AuthTextField( + labelText: localization.usernameOrEmail, + keyboardType: TextInputType.emailAddress, + assetIconPath: Assets.profileIcon, + onChanged: (String text) => provider.username = text, + ), + AuthTextField( + labelText: localization.password, + obscureText: true, + keyboardType: TextInputType.visiblePassword, + assetIconPath: Assets.passwordIcon, + onChanged: (String text) => provider.password = text, + ), + ], + ), + ), + const SizedBox(height: Insets.x3_5), + PrimaryButtonWidget( + text: localization.logIn, + assetIconPath: Assets.arrowRightIcon, + onPressedFunction: () => provider.tryLogin(), + ), + const SizedBox(height: Insets.x8_5), + AuthRichText(textSpanModelList: provider.bottomText), + ], + ); + } +} diff --git a/ecommers/lib/ui/pages/authorization/sign_up_page.dart b/ecommers/lib/ui/pages/authorization/sign_up_page.dart new file mode 100644 index 0000000..c5d55ed --- /dev/null +++ b/ecommers/lib/ui/pages/authorization/sign_up_page.dart @@ -0,0 +1,75 @@ +import 'package:ecommers/core/provider_models/sign_up_provider_model.dart'; +import 'package:ecommers/generated/i18n.dart'; +import 'package:ecommers/ui/decorations/assets.dart'; +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/pages/authorization/index.dart'; +import 'package:ecommers/ui/widgets/authorization/index.dart'; +import 'package:ecommers/ui/widgets/button/index.dart'; +import 'package:ecommers/ui/widgets/progress.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class SignUpPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Consumer( + builder: (_, SignUpProviderModel provider, child) { + return Stack( + children: [ + child, + Visibility( + visible: provider.isBusy, + child: const Progress(), + ), + ], + ); + }, + child: _buildContent(context), + ); + } + + Widget _buildContent(BuildContext context) { + final localization = I18n.of(context); + final provider = Provider.of(context, listen: false); + + return AuthorizationTabBase( + children: [ + const SizedBox(height: Insets.x5), + AuthTextFieldAreaContainer( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AuthTextField( + labelText: localization.email, + keyboardType: TextInputType.emailAddress, + assetIconPath: Assets.mailIcon, + onChanged: (text) => provider.email = text, + ), + AuthTextField( + labelText: localization.username, + assetIconPath: Assets.profileIcon, + onChanged: (text) => provider.username = text, + ), + AuthTextField( + labelText: localization.password, + obscureText: true, + keyboardType: TextInputType.visiblePassword, + assetIconPath: Assets.passwordIcon, + onChanged: (text) => provider.password = text, + ), + ], + ), + ), + const SizedBox(height: Insets.x3_5), + PrimaryButtonWidget( + text: localization.signUp, + assetIconPath: Assets.arrowRightIcon, + onPressedFunction: () => provider.tryAuthorize(), + ), + const SizedBox(height: Insets.x8_5), + AuthRichText(textSpanModelList: provider.bottomText), + ], + ); + } +} diff --git a/ecommers/lib/ui/pages/cart_page.dart b/ecommers/lib/ui/pages/cart_page.dart new file mode 100644 index 0000000..60ebefd --- /dev/null +++ b/ecommers/lib/ui/pages/cart_page.dart @@ -0,0 +1,127 @@ +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/core/models/index.dart'; +import 'package:ecommers/core/services/index.dart'; +import 'package:ecommers/generated/i18n.dart'; +import 'package:ecommers/ui/decorations/assets.dart'; +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/widgets/order/index.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class CartPage extends StatefulWidget { + @override + _CartPageState createState() => _CartPageState(); +} + +class _CartPageState extends State { + static const _orderDeviderIndent = 100.0; + + static String _getDressAssetPath(int index) { + final modulo = index % 7; + + if (modulo == 0) return Assets.dressCottonImage; + if (modulo == 1) return Assets.dressFloral2Image; + if (modulo == 2) return Assets.dressFloralImage; + if (modulo == 3) return Assets.dressPattern2Image; + if (modulo == 4) return Assets.dressPatternImage; + if (modulo == 5) return Assets.dressCotton2Image; + if (modulo == 6) { + return Assets.greenBackpackImage; + } else { + return Assets.greenBackpackImage; + } + } + + BuildContext _context; + final _orders = List.generate( + 20, + (index) => OrderModel( + title: 'Bottle Green Backpack', + description: 'Medium, Green', + cost: 2.58, + imagePath: _getDressAssetPath(index), + count: 1), + ); + + void decrementCount(OrderModel order) { + if (order.count == 1) _orders.remove(order); + + setState(() { + order.count--; + }); + } + + void incrementCount(OrderModel order) { + setState(() { + order.count++; + }); + } + + @override + Widget build(BuildContext context) { + _context = context; + final double totalOrderCost = _orders.fold(0.0, + (totalCost, nextOrder) => totalCost + nextOrder.count * nextOrder.cost); + + return Padding( + padding: + const EdgeInsets.fromLTRB(Insets.x6, Insets.x0, Insets.x5, Insets.x4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + I18n.of(context).cartTitle, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 16), + Expanded( + child: _buildOrderListView(), + ), + const Padding( + padding: EdgeInsets.fromLTRB( + Insets.x0, Insets.x4, Insets.x0, Insets.x2), + child: Divider(color: BrandingColors.secondary), + ), + TotalOrderWidget( + cost: totalOrderCost, + backgroundColor: BrandingColors.pageBackground, + onButtonPressedFunction: () => + navigationService.navigateTo(Pages.checkout), + buttonText: I18n.of(_context).checkoutButton, + ) + ], + ), + ); + } + + Widget _buildOrderListView() { + return ListView.separated( + padding: + const EdgeInsets.fromLTRB(Insets.x0, Insets.x0, Insets.x5, Insets.x0), + itemCount: _orders.length, + itemBuilder: (BuildContext context, int index) { + final currentOrder = _orders[index]; + return OrderWidget( + primaryText: currentOrder.title, + secondaryText: currentOrder.description, + assetImagePath: currentOrder.imagePath, + cost: currentOrder.cost, + count: currentOrder.count, + countIncrementFunction: () => incrementCount(currentOrder), + countDecrementFunction: () => decrementCount(currentOrder), + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Padding( + padding: const EdgeInsets.fromLTRB( + Insets.x0, Insets.x3, Insets.x0, Insets.x4), + child: Divider( + color: BrandingColors.secondary.withOpacity(0.4), + indent: _orderDeviderIndent, + ), + ); + }, + ); + } +} diff --git a/ecommers/lib/ui/pages/categories_page.dart b/ecommers/lib/ui/pages/categories_page.dart new file mode 100644 index 0000000..32a26df --- /dev/null +++ b/ecommers/lib/ui/pages/categories_page.dart @@ -0,0 +1,138 @@ +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/generated/i18n.dart'; +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/pages/closeable_page.dart'; +import 'package:ecommers/ui/widgets/backgrounded_safe_area.dart'; +import 'package:ecommers/ui/widgets/category_item/category_item.dart'; +import 'package:ecommers/ui/widgets/menu/index.dart'; +import 'package:flutter/material.dart'; + +const List _topMenuList = [ + MenuItemModel( + title: 'T-shirt', + ), + MenuItemModel( + title: 'Shirts', + ), + MenuItemModel( + title: 'Pants & Jeans', + ), + MenuItemModel( + title: 'Socks & Ties', + ), + MenuItemModel( + title: 'Underwear', + ), + MenuItemModel( + title: 'Jackets', + ), + MenuItemModel( + title: 'Coats', + ), + MenuItemModel( + title: 'Sweaters', + ), +]; + +const List _bottomMenuList = [ + MenuItemModel( + title: 'Officewear', + ), + MenuItemModel( + title: 'Blouce & T-Shirts', + ), + MenuItemModel( + title: 'Pants & Jeans', + ), + MenuItemModel( + title: 'Dresses', + ), + MenuItemModel( + title: 'Lingerie', + ), + MenuItemModel( + title: 'Jackets', + ), + MenuItemModel( + title: 'Coats', + ), + MenuItemModel( + title: 'Sweaters', + ), +]; + +class CategoriesPage extends StatelessWidget { + const CategoriesPage(); + + @override + Widget build(BuildContext context) { + return CloseablePage( + child: BackgroundedSafeArea( + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + padding: + const EdgeInsets.fromLTRB(Insets.x5, 0, Insets.x5, Insets.x5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + I18n.of(context).allCategories, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: Insets.x6), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildCategories(), + const SizedBox(width: Insets.x6), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const MenuList( + title: 'MEN\'S APPAREL', + itemHeight: 44.0, + itemList: _topMenuList, + ), + Divider( + height: 52.0, + thickness: 1.0, + color: BrandingColors.secondary.withOpacity(0.1), + ), + const MenuList( + title: 'WOMEN\'S APPAREL', + itemHeight: 44.0, + itemList: _bottomMenuList, + ), + ], + ), + ) + ], + ), + ], + ), + ), + ), + ); + } + + Widget _buildCategories() { + return SizedBox( + width: CategoryItem.size.width, + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: Categories.values.length, + separatorBuilder: (BuildContext context, int index) => const SizedBox( + width: Insets.x8, + height: Insets.x8, + ), + scrollDirection: Axis.vertical, + itemBuilder: (BuildContext context, int index) { + return CategoryItem.fromType(Categories.values[index]); + }, + ), + ); + } +} diff --git a/ecommers/lib/ui/pages/checkout_page.dart b/ecommers/lib/ui/pages/checkout_page.dart new file mode 100644 index 0000000..82cb579 --- /dev/null +++ b/ecommers/lib/ui/pages/checkout_page.dart @@ -0,0 +1,290 @@ +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/core/models/index.dart'; +import 'package:ecommers/core/services/index.dart'; +import 'package:ecommers/generated/i18n.dart'; +import 'package:ecommers/ui/decorations/assets.dart'; +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/pages/closeable_page.dart'; +import 'package:ecommers/ui/widgets/circle_icon.dart'; +import 'package:ecommers/ui/widgets/index.dart'; +import 'package:ecommers/ui/widgets/order/index.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:keyboard_visibility/keyboard_visibility.dart'; + +class CheckoutPage extends StatefulWidget { + @override + _CheckoutPageState createState() => _CheckoutPageState(); +} + +class _CheckoutPageState extends State { + final KeyboardVisibilityNotification keyboardVisibilityNotification = + KeyboardVisibilityNotification(); + bool isTotalOrderVisible = true; + + @override + void initState() { + super.initState(); + + keyboardVisibilityNotification.addNewListener( + onChange: (bool visible) { + setState(() { + isTotalOrderVisible = !visible; + }); + }, + ); + } + + @override + void dispose() { + keyboardVisibilityNotification.dispose(); + super.dispose(); + } + + static const int _itemCount = 20; + + static String _getDressAssetPath(int index) { + final modulo = index % 7; + + if (modulo == 0) return Assets.dressCottonImage; + if (modulo == 1) return Assets.dressFloral2Image; + if (modulo == 2) return Assets.dressFloralImage; + if (modulo == 3) return Assets.dressPattern2Image; + if (modulo == 4) return Assets.dressPatternImage; + if (modulo == 5) return Assets.dressCotton2Image; + if (modulo == 6) { + return Assets.greenBackpackImage; + } else { + return Assets.greenBackpackImage; + } + } + + final _orders = List.generate( + _itemCount, + (index) => OrderModel( + title: 'Bottle Green Backpack', + description: 'Medium, Green', + cost: 2.58, + imagePath: _getDressAssetPath(index), + count: 1)); + + @override + Widget build(BuildContext context) { + final double totalOrderCost = _orders.fold(0.0, + (totalCost, nextOrder) => totalCost + nextOrder.count * nextOrder.cost); + + return CloseablePage( + child: Column( + children: [ + Expanded( + child: BackgroundedSafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB( + Insets.x6, Insets.x0, Insets.x5, Insets.x4), + child: _buildOrderListView(), + ), + ), + ), + Visibility( + visible: isTotalOrderVisible, + child: TotalOrderWidget( + cost: totalOrderCost, + backgroundColor: BrandingColors.background, + onButtonPressedFunction: () => + navigationService.navigateTo(Pages.success), + buttonText: I18n.of(context).placeOrderButton, + padding: const EdgeInsets.fromLTRB( + Insets.x6, Insets.x2, Insets.x5, Insets.x3_5), + ), + ), + ], + ), + ); + } + + Widget _buildListHeader() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + I18n.of(context).checkoutTitle, + style: Theme.of(context).textTheme.headline6, + ), + const SizedBox(height: 20), + Text( + I18n.of(context).shippingAddress, + style: Theme.of(context).textTheme.headline5, + ), + const SizedBox(height: Insets.x2), + _buildShippingAddress(), + _buildDevider(), + Text( + I18n.of(context).paymentMethod, + style: Theme.of(context).textTheme.headline5, + ), + const SizedBox(height: Insets.x2), + _buildRowAction( + imagePath: Assets.creditCardImage, + text: Text( + I18n.of(context).cardEnding, + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(fontWeight: FontWeight.w700), + ), + ), + _buildDevider(), + Text( + I18n.of(context).items, + style: Theme.of(context).textTheme.headline5, + ), + const SizedBox(height: 14.0), + ], + ); + } + + Widget _buildListFooter() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDevider(), + SizedBox( + height: 28, + child: TextField( + decoration: InputDecoration( + border: InputBorder.none, + hintText: 'Message to seller (optional)', + hintStyle: Theme.of(context).textTheme.bodyText1.copyWith( + fontWeight: FontWeight.w300, + fontStyle: FontStyle.italic, + ), + ), + ), + ), + _buildDevider(), + _buildRowAction( + imagePath: Assets.saleImage, + text: Text( + I18n.of(context).addPromoCode, + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(color: BrandingColors.primary), + ), + ), + ], + ); + } + + Widget _buildOrderListView() { + final int newItemCount = _orders.length + 2; + return ListView.separated( + itemCount: newItemCount, + itemBuilder: (BuildContext context, int index) { + if (index == 0) { + return _buildListHeader(); + } + + if (index == newItemCount - 1) { + return _buildListFooter(); + } + + final currentOrder = _orders[index - 1]; + return SmallOrderWidget( + primaryText: currentOrder.title, + secondaryText: currentOrder.description, + assetImagePath: currentOrder.imagePath, + cost: currentOrder.cost, + count: currentOrder.count, + countIncrementFunction: () => incrementCount(currentOrder), + countDecrementFunction: () => decrementCount(currentOrder), + ); + }, + separatorBuilder: (BuildContext context, int index) { + if (index == 0) { + return const SizedBox(height: 0); + } + if (index == newItemCount - 2) { + return const SizedBox(height: 0); + } + return Padding( + padding: const EdgeInsets.fromLTRB( + Insets.x0, Insets.x3, Insets.x0, Insets.x4), + child: Divider( + color: BrandingColors.secondary.withOpacity(0.4), + indent: 83.0, + ), + ); + }, + ); + } + + void decrementCount(OrderModel order) { + if (order.count == 1) _orders.remove(order); + + setState(() { + order.count--; + }); + } + + void incrementCount(OrderModel order) { + setState(() { + order.count++; + }); + } + + Widget _buildShippingAddress() { + return Row( + children: [ + Container( + width: 136, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'John Doe', //TODO from provider + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(fontWeight: FontWeight.w700), + ), + Text( + 'No 123, Sub Street, Main Street,City Name, Province, Country', + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(fontWeight: FontWeight.w400), + ), + ], + ), + ), + const Spacer(), + const CircleIcon(), + ], + ); + } + + Widget _buildRowAction({String imagePath, Text text}) { + return Row( + children: [ + Image.asset( + imagePath, + fit: BoxFit.scaleDown, + ), + const SizedBox(width: Insets.x3_5), + text, + const Spacer(), + const CircleIcon(), + ], + ); + } + + Widget _buildDevider() { + return Divider( + color: BrandingColors.secondary.withOpacity(0.15), + thickness: 1.0, + height: 24.0, + ); + } +} diff --git a/ecommers/lib/ui/pages/closeable_page.dart b/ecommers/lib/ui/pages/closeable_page.dart new file mode 100644 index 0000000..a0166da --- /dev/null +++ b/ecommers/lib/ui/pages/closeable_page.dart @@ -0,0 +1,31 @@ +import 'package:ecommers/core/services/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class CloseablePage extends StatelessWidget { + final Widget child; + + const CloseablePage({this.child}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: BrandingColors.pageBackground, + appBar: AppBar( + automaticallyImplyLeading: false, + actions: [ + IconButton( + icon: SvgPicture.asset( + Assets.closeIcon, + color: BrandingColors.primary, + height: 18.0, + ), + onPressed: () => navigationService.goBack(), + ) + ], + ), + body: child, + ); + } +} diff --git a/ecommers/lib/ui/pages/home_page.dart b/ecommers/lib/ui/pages/home_page.dart new file mode 100644 index 0000000..f2fff94 --- /dev/null +++ b/ecommers/lib/ui/pages/home_page.dart @@ -0,0 +1,119 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:ecommers/generated/i18n.dart'; +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/widgets/category_item/categories_compact_widget.dart'; +import 'package:ecommers/ui/widgets/index.dart'; +import 'package:ecommers/ui/widgets/product_item/product_item_normal.dart'; +import 'package:flutter/material.dart'; + +class HomePage extends StatelessWidget { + static const double _latestGridViewAxisSpacing = 12.0; + static const imageCardSize = Size(325.0, 184.0); + static const productItemNormalSize = Size(101.0, 135.0); + + @override + Widget build(BuildContext context) { + return CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + CategoriesCompactWidget(), + const SizedBox(height: Dimens.pagePadding), + Padding( + padding: const EdgeInsets.symmetric(horizontal: Insets.x6), + child: Text( + I18n.of(context).latetstTitle, + style: Theme.of(context).textTheme.headline6, + ), + ), + const SizedBox(height: 10.0), + _buildLatestCarousel(context), + ], + ), + ), + _buildLatestGridView(context), + ], + ); + } + + Widget _buildLatestCarousel(BuildContext context) { + return CarouselSlider( + viewportFraction: 0.92, + items: List.generate( + 6, + (index) { + return SizedBox.expand( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: Insets.x2), + child: ImageCard( + buttonText: 'SEE MORE', + description: 'For all your summer clothing needs', + imageAsset: getCarouselImage(index), + onButtonPressed: () {}, + ), + ), + ); + }, + ), + ); + } + + String getCarouselImage(int index) { + final modulo = index % 3; + + if (modulo == 0) { + return Assets.girlImage; + } else if (modulo == 1) { + return Assets.girl2Image; + } else { + return Assets.girl3Image; + } + } + + Widget _buildLatestGridView(BuildContext context) { + return SliverPadding( + padding: const EdgeInsets.symmetric( + horizontal: Dimens.pagePadding, + vertical: Insets.x2_5, + ), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: ProductItemNormal.size.height, + mainAxisSpacing: _latestGridViewAxisSpacing, + crossAxisSpacing: _latestGridViewAxisSpacing, + childAspectRatio: + ProductItemNormal.size.width / ProductItemNormal.size.height, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + return ProductItemNormal( + assetImagePath: _getDressAssetPath(index), + cost: 15.0, + title: 'best dress ever', + rate: 3.9, + ); + }, + childCount: 30, + ), + ), + ); + } + + String _getDressAssetPath(int index) { + final modulo = index % 6; + + if (modulo == 0) return Assets.dressCottonImage; + if (modulo == 1) return Assets.dressFloral2Image; + if (modulo == 2) return Assets.dressFloralImage; + if (modulo == 3) return Assets.dressPattern2Image; + if (modulo == 4) return Assets.dressPatternImage; + if (modulo == 5) { + return Assets.dressCotton2Image; + } else { + return Assets.greenBackpackImage; + } + } +} diff --git a/ecommers/lib/ui/pages/index.dart b/ecommers/lib/ui/pages/index.dart new file mode 100644 index 0000000..4840cf3 --- /dev/null +++ b/ecommers/lib/ui/pages/index.dart @@ -0,0 +1,10 @@ +export 'cart_page.dart'; +export 'categories_page.dart'; +export 'checkout_page.dart'; +export 'closeable_page.dart'; +export 'home_page.dart'; +export 'more_page.dart'; +export 'profile_page.dart'; +export 'search_page.dart'; +export 'shell_page.dart'; +export 'success_page.dart'; diff --git a/ecommers/lib/ui/pages/more_page.dart b/ecommers/lib/ui/pages/more_page.dart new file mode 100644 index 0000000..8200a79 --- /dev/null +++ b/ecommers/lib/ui/pages/more_page.dart @@ -0,0 +1,105 @@ +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/core/services/index.dart'; +import 'package:ecommers/generated/i18n.dart'; +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/widgets/menu/index.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class MorePage extends StatelessWidget { + static const List topMenuList = [ + MenuItemModel( + svgAssetIconPath: Assets.shippingIcon, + title: 'Shipping Adress', + ), + MenuItemModel( + svgAssetIconPath: Assets.paymentIcon, + title: 'Payment Method', + ), + MenuItemModel( + svgAssetIconPath: Assets.currencyIcon, + title: 'Currency', + subTitle: 'USD', + ), + MenuItemModel( + svgAssetIconPath: Assets.languageIcon, + title: 'Language', + subTitle: 'ENGLISH', + ), + ]; + static const List bottomMenuList = [ + MenuItemModel( + svgAssetIconPath: Assets.bellIcon, + title: 'Notification Settings', + ), + MenuItemModel( + svgAssetIconPath: Assets.shieldIcon, + title: 'Privacy Policy', + ), + MenuItemModel( + svgAssetIconPath: Assets.discussIssueIcon, + title: 'Frequently Asked Questions', + ), + MenuItemModel( + svgAssetIconPath: Assets.checkFormIcon, + title: 'Legal Information', + ), + ]; + + static const EdgeInsets menuListMargin = + EdgeInsets.symmetric(horizontal: Insets.x5); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: Insets.x6), + child: Text( + I18n.of(context).morePage, + style: Theme.of(context).textTheme.headline6, + ), + ), + const SizedBox(height: 35.0), + const MenuList( + margin: menuListMargin, + itemList: topMenuList, + ), + const SizedBox(height: 15.0), + const MenuList( + margin: menuListMargin, + itemList: bottomMenuList, + ), + const SizedBox(height: 40.0), + _buildLogOutButton(context), + const SizedBox(height: 30.0), + ], + ), + ); + } + + Widget _buildLogOutButton(BuildContext context) { + return Center( + child: CupertinoButton( + onPressed: logOutPressHandler, + child: Text( + I18n.of(context).logOut, + style: Theme.of(context) + .textTheme + .headline5 + .apply(color: BrandingColors.primary), + ), //TODO use proovider + ), + ); + } + + Future logOutPressHandler() async { + await authorizationService.logOut(); + await navigationService.navigateWithReplacementTo(Pages.authorization); + } +} diff --git a/ecommers/lib/ui/pages/profile_page.dart b/ecommers/lib/ui/pages/profile_page.dart new file mode 100644 index 0000000..ca04d2d --- /dev/null +++ b/ecommers/lib/ui/pages/profile_page.dart @@ -0,0 +1,142 @@ +import 'package:ecommers/generated/i18n.dart'; +import 'package:ecommers/ui/decorations/assets.dart'; +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/widgets/menu/index.dart'; +import 'package:ecommers/ui/widgets/menu/menu_item_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +class ProfilePage extends StatelessWidget { + static const List _topMenuList = [ + MenuItemModel( + svgAssetIconPath: Assets.allOrderIcon, + title: 'All My Orders', + ), + MenuItemModel( + svgAssetIconPath: Assets.pendingShipmentIcon, + title: 'Pending Shipments', + ), + MenuItemModel( + svgAssetIconPath: Assets.pendingPaymentIcon, + title: 'Pending Payments', + ), + MenuItemModel( + svgAssetIconPath: Assets.finishedOrdersIcon, + title: 'Finished Orders', + ), + ]; + + static const List _bottomMenuList = [ + MenuItemModel( + svgAssetIconPath: Assets.inviteFriendsIcon, + title: 'Invite Friends', + ), + MenuItemModel( + svgAssetIconPath: Assets.supportIcon, + title: 'Customer Support', + ), + MenuItemModel( + svgAssetIconPath: Assets.rateAppIcon, + title: 'Rate Our App', + ), + MenuItemModel( + svgAssetIconPath: Assets.suggestIcon, + title: 'Make a Suggestion', + ), + ]; + + static const EdgeInsets _listContainerMargin = + EdgeInsets.symmetric(horizontal: Insets.x5); + + static const double _profileCardHeight = 100.0; + static const double _profileCardEditButtonHeight = 30.0; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + children: [ + _buildProfileCard(context), + const MenuList( + margin: _listContainerMargin, + itemList: _topMenuList, + ), + const SizedBox(height: 15.0), + const MenuList( + margin: _listContainerMargin, + itemList: _bottomMenuList, + ), + const SizedBox(height: 20.0), + ], + ), + ); + } + + Widget _buildProfileCard(BuildContext context) { + return Container( + height: _profileCardHeight, + margin: const EdgeInsets.all(Insets.x6), + child: Row( + children: [ + Container( + height: _profileCardHeight, + width: _profileCardHeight, + decoration: const BoxDecoration( + shape: BoxShape.circle, + image: DecorationImage( + image: AssetImage(Assets.girlImage), + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(width: 20.0), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Jane Doe', //TODO: get from the provider + maxLines: Dimens.defaultTextMaxLines, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.headline6, + ), + Expanded( + child: Text( + 'janedoe123@email.com', //TODO: get from the provider + maxLines: Dimens.defaultTextMaxLines, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText1, + ), + ), + _buildEditProfileButton(context), + ], + ), + ), + ], + ), + ); + } + + Widget _buildEditProfileButton(BuildContext context) { + return SizedBox( + height: _profileCardEditButtonHeight, + child: OutlineButton( + borderSide: BorderSide( + color: BrandingColors.secondary.withOpacity(0.3), + width: 1.0, + ), + highlightedBorderColor: BrandingColors.secondary.withOpacity(0.3), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(Radiuses.big_2x), + ), + onPressed: () {}, + child: Text( + I18n.of(context).editProfile, + style: Theme.of(context).textTheme.button, + ), + ), + ); + } +} diff --git a/ecommers/lib/ui/pages/search_page.dart b/ecommers/lib/ui/pages/search_page.dart new file mode 100644 index 0000000..9175b75 --- /dev/null +++ b/ecommers/lib/ui/pages/search_page.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +class SearchPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + return const Text('Search'); + } +} diff --git a/ecommers/lib/ui/pages/shell_page.dart b/ecommers/lib/ui/pages/shell_page.dart new file mode 100644 index 0000000..f027691 --- /dev/null +++ b/ecommers/lib/ui/pages/shell_page.dart @@ -0,0 +1,83 @@ +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/core/provider_models/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/notifier_provider_widget.dart'; +import 'package:ecommers/ui/pages/index.dart'; +import 'package:ecommers/ui/widgets/bottom_navigation/bottom_navigation_widget.dart'; +import 'package:ecommers/ui/widgets/index.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class ShellPage extends StatefulWidget { + @override + _ShellPageState createState() => _ShellPageState(); +} + +class _ShellPageState extends State { + @override + Widget build(BuildContext context) { + return NotifierProviderWidget( + providerModel: ShellProviderModel(), + builder: (context, ShellProviderModel model, child) { + return Scaffold( + appBar: AppBar( + actions: [ + _buildAction( + imageAssetPath: Assets.messagesIcon, + onIconPressedFuction: () {}, //TODO get from provider + badgeValue: 5, //TODO get from provider + ), + _buildAction( + imageAssetPath: Assets.notificationIcon, + onIconPressedFuction: () {}, //TODO get from provider + badgeValue: 6, //TODO get from provider + ), + ], + ), + backgroundColor: BrandingColors.pageBackground, + body: BackgroundedSafeArea( + child: _buildBody(model.selectedPage), + ), + bottomNavigationBar: BottomNavigationWidget( + selectedIndex: model.selectedItemIndex, + pages: model.pages, + onTappedFunction: model.onTappedItem, + orderCount: 3, + ), + ); + }, + ); + } + + Widget _buildBody(Pages pageType) { + switch (pageType) { + case Pages.home: + return HomePage(); + case Pages.search: + return SearchPage(); + case Pages.cart: + return CartPage(); + case Pages.profile: + return ProfilePage(); + case Pages.more: + return MorePage(); + default: + return HomePage(); + } + } + + Widget _buildAction({ + String imageAssetPath, + Function() onIconPressedFuction, + int badgeValue, + }) { + return IconButton( + icon: IconWithBadge( + badgeValue: badgeValue, + badgeTextStyle: Theme.of(context).textTheme.overline, + icon: SvgPicture.asset(imageAssetPath), + ), + onPressed: onIconPressedFuction, + ); + } +} diff --git a/ecommers/lib/ui/pages/success_page.dart b/ecommers/lib/ui/pages/success_page.dart new file mode 100644 index 0000000..8b857f5 --- /dev/null +++ b/ecommers/lib/ui/pages/success_page.dart @@ -0,0 +1,57 @@ +import 'package:ecommers/generated/i18n.dart'; +import 'package:ecommers/ui/decorations/assets.dart'; +import 'package:ecommers/ui/pages/closeable_page.dart'; +import 'package:ecommers/ui/widgets/button/index.dart'; +import 'package:ecommers/ui/widgets/order/index.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class SuccessPage extends StatelessWidget { + static const circleImageSize = Size(101.0, 101.0); + + const SuccessPage(); + + @override + Widget build(BuildContext context) { + return CloseablePage( + child: SingleChildScrollView( + child: Align( + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircleImage( + image: SvgPicture.asset( + Assets.successIcon, + fit: BoxFit.scaleDown, + ), + size: circleImageSize, + ), + const SizedBox(height: 28.0), + Text('John Doe', //TODO from provider + style: Theme.of(context).textTheme.headline6), + const SizedBox(height: 14.0), + SizedBox( + width: 252.0, + child: Text( + I18n.of(context).successMessage, + style: Theme.of(context).textTheme.subtitle1, + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 50.0), + SizedBox( + width: 165.0, + child: PrimaryButtonWidget( + text: 'MY ORDERS', + onPressedFunction: () {}, + ), + ), + const SizedBox(height: 20.0), + ], + ), + ), + ), + ); + } +} diff --git a/ecommers/lib/ui/utils/dialog_manager.dart b/ecommers/lib/ui/utils/dialog_manager.dart new file mode 100644 index 0000000..eebd6c1 --- /dev/null +++ b/ecommers/lib/ui/utils/dialog_manager.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +class DialogManager { + static Future showAlertDialog( + BuildContext context, String title, String contentText) async { + await showDialog( + context: context, + builder: (_) => AlertDialog( + elevation: 10.0, + title: Text(title), + content: Text(contentText), + ), + ); + } +} diff --git a/ecommers/lib/ui/utils/formatter.dart b/ecommers/lib/ui/utils/formatter.dart new file mode 100644 index 0000000..cdd8c2d --- /dev/null +++ b/ecommers/lib/ui/utils/formatter.dart @@ -0,0 +1,16 @@ +import 'package:intl/intl.dart'; + +class Formatter { + static String getCost(double cost) { + final currencyFormatter = NumberFormat.simpleCurrency(); + + return currencyFormatter.format(cost); + } + static String getTextWithNumberCard(String nuberCard){ + const visibleCardSymbolCount = 2; + + final visibleSymbols = nuberCard.substring(nuberCard.length - visibleCardSymbolCount); + + return 'Master Card ending **$visibleSymbols'; + } +} \ No newline at end of file diff --git a/ecommers/lib/ui/widgets/authorization/auth_rich_text.dart b/ecommers/lib/ui/widgets/authorization/auth_rich_text.dart new file mode 100644 index 0000000..dacd905 --- /dev/null +++ b/ecommers/lib/ui/widgets/authorization/auth_rich_text.dart @@ -0,0 +1,111 @@ +import 'package:ecommers/core/models/index.dart'; +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class AuthRichText extends StatefulWidget { + final List textSpanModelList; + + const AuthRichText({this.textSpanModelList}); + + @override + _AuthRichTextState createState() => _AuthRichTextState(); +} + +class _AuthRichTextState extends State { + List _textTapRecognizerList; + TextStyle baseTextStyle; + + @override + void dispose() { + _textTapRecognizerList.forEach(disposeRecognizer); + super.dispose(); + } + + @override + void initState() { + _textTapRecognizerList = []; + super.initState(); + } + + void disposeRecognizer(TapGestureRecognizer recognizer) { + recognizer.dispose(); + } + + @override + Widget build(BuildContext context) { + baseTextStyle = Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: FontSizes.small_3x, + ); + + final firstTextSpanModel = widget.textSpanModelList[0]; + final childTextSpanModelList = widget.textSpanModelList.skip(1).toList(); + + return RichText( + textAlign: TextAlign.center, + text: _buildTextSpan(firstTextSpanModel, childTextSpanModelList), + ); + } + + TextSpan _buildTextSpan(AuthRichTextSpanModel textModel, + [List childens]) { + return textModel.isTappable + ? _buildTapableTextSpan(textModel, childens) + : _buildDefaultTextSpan(textModel, childens); + } + + TextSpan _buildDefaultTextSpan(AuthRichTextSpanModel textModel, + [List childens]) { + return TextSpan( + text: textModel.text, + style: baseTextStyle, + children: [..._buildChildTextSpanList(childens)], + ); + } + + TextSpan _buildTapableTextSpan(AuthRichTextSpanModel textModel, + [List childens]) { + return TextSpan( + text: textModel.text, + style: baseTextStyle.copyWith(color: BrandingColors.primary), + recognizer: _getRecognizerFor(textModel), + children: [..._buildChildTextSpanList(childens)], + ); + } + + Iterable _buildChildTextSpanList( + List childTextSpanModelList) sync* { + if (childTextSpanModelList != null) { + for (final item in childTextSpanModelList) { + yield _buildTextSpan(item); + } + } + } + + TapGestureRecognizer _getRecognizerFor(AuthRichTextSpanModel textModel) { + if (!textModel.isTappable) return null; + + if (_textTapRecognizerList.isEmpty) { + return _createRecognizer(textModel.onTap); + } + + final recogizerIndex = widget.textSpanModelList + .where((element) => element.isTappable) + .toList() + .indexOf(textModel); + + if (recogizerIndex < _textTapRecognizerList.length && recogizerIndex >= 0) { + return _textTapRecognizerList[recogizerIndex]; + } + + return _createRecognizer(textModel.onTap); + } + + TapGestureRecognizer _createRecognizer(Function() onTap) { + final recognizer = TapGestureRecognizer()..onTap = onTap; + _textTapRecognizerList.add(recognizer); + + return recognizer; + } +} diff --git a/ecommers/lib/ui/widgets/authorization/auth_text_field.dart b/ecommers/lib/ui/widgets/authorization/auth_text_field.dart new file mode 100644 index 0000000..3aff641 --- /dev/null +++ b/ecommers/lib/ui/widgets/authorization/auth_text_field.dart @@ -0,0 +1,48 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/svg.dart'; + +class AuthTextField extends StatelessWidget { + final TextEditingController controller; + final String labelText; + final String assetIconPath; + final TextInputType keyboardType; + final bool obscureText; + final Function(String) onChanged; + + const AuthTextField({ + this.labelText = '', + this.assetIconPath = '', + this.keyboardType = TextInputType.text, + this.obscureText = false, + this.controller, + this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + keyboardType: keyboardType, + obscureText: obscureText, + onChanged: onChanged, + decoration: InputDecoration( + labelText: labelText, + labelStyle: Theme.of(context).textTheme.headline5, + prefixIcon: SvgPicture.asset( + assetIconPath, + fit: BoxFit.scaleDown, + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), + ), + fillColor: Colors.transparent, + filled: true, + ), + ); + } +} diff --git a/ecommers/lib/ui/widgets/authorization/auth_text_field_area_container.dart b/ecommers/lib/ui/widgets/authorization/auth_text_field_area_container.dart new file mode 100644 index 0000000..f75f62e --- /dev/null +++ b/ecommers/lib/ui/widgets/authorization/auth_text_field_area_container.dart @@ -0,0 +1,28 @@ +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:flutter/material.dart'; + +class AuthTextFieldAreaContainer extends StatelessWidget { + final Widget child; + + const AuthTextFieldAreaContainer({this.child}); + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(Radiuses.normal), + color: BrandingColors.background, + boxShadow: const [ + BoxShadow( + offset: Dimens.defaultBlurOffset, + blurRadius: Radiuses.big_1x, + color: BrandingColors.blur, + ) + ], + ), + child: child, + ); + } +} \ No newline at end of file diff --git a/ecommers/lib/ui/widgets/authorization/index.dart b/ecommers/lib/ui/widgets/authorization/index.dart new file mode 100644 index 0000000..9746121 --- /dev/null +++ b/ecommers/lib/ui/widgets/authorization/index.dart @@ -0,0 +1,3 @@ +export 'auth_rich_text.dart'; +export 'auth_text_field.dart'; +export 'auth_text_field_area_container.dart'; diff --git a/ecommers/lib/ui/widgets/backgrounded_safe_area.dart b/ecommers/lib/ui/widgets/backgrounded_safe_area.dart new file mode 100644 index 0000000..5cf1c3a --- /dev/null +++ b/ecommers/lib/ui/widgets/backgrounded_safe_area.dart @@ -0,0 +1,21 @@ +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:flutter/material.dart'; + +class BackgroundedSafeArea extends StatelessWidget { + final Widget child; + final bool isBottom; + + const BackgroundedSafeArea({ + Key key, + this.child, + this.isBottom = true, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + color: BrandingColors.pageBackground, + child: SafeArea(bottom: isBottom, child: child), + ); + } +} diff --git a/ecommers/lib/ui/widgets/bottom_navigation/bottom_navigation_item_model.dart b/ecommers/lib/ui/widgets/bottom_navigation/bottom_navigation_item_model.dart new file mode 100644 index 0000000..a60610d --- /dev/null +++ b/ecommers/lib/ui/widgets/bottom_navigation/bottom_navigation_item_model.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class BottomNavigationItemModel { + final IconData icon; + final String title; + + BottomNavigationItemModel({ + @required this.icon, + @required this.title, + }); +} diff --git a/ecommers/lib/ui/widgets/bottom_navigation/bottom_navigation_widget.dart b/ecommers/lib/ui/widgets/bottom_navigation/bottom_navigation_widget.dart new file mode 100644 index 0000000..3ee76d2 --- /dev/null +++ b/ecommers/lib/ui/widgets/bottom_navigation/bottom_navigation_widget.dart @@ -0,0 +1,56 @@ +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/widgets/bottom_navigation/buttom_bar_item_icon.dart'; +import 'package:ecommers/ui/widgets/bottom_navigation/consts.dart'; +import 'package:flutter/material.dart'; + +class BottomNavigationWidget extends StatefulWidget { + final Iterable pages; + final int selectedIndex; + final Function(int) onTappedFunction; + final int orderCount; + + const BottomNavigationWidget({ + @required this.pages, + @required this.selectedIndex, + @required this.onTappedFunction, + this.orderCount = 0, + }); + + @override + _BottomNavigationWidgetState createState() => _BottomNavigationWidgetState(); +} + +class _BottomNavigationWidgetState extends State { + @override + Widget build(BuildContext context) { + return BottomNavigationBar( + backgroundColor: BrandingColors.background, + items: _createBottomNavigationBarItems(), + unselectedItemColor: BrandingColors.primaryText, + selectedItemColor: BrandingColors.primary, + currentIndex: widget.selectedIndex, + selectedLabelStyle: Theme.of(context).textTheme.caption, + unselectedLabelStyle: Theme.of(context).textTheme.caption, + iconSize: 26.0, + showUnselectedLabels: true, + type: BottomNavigationBarType.fixed, + onTap: widget.onTappedFunction, + ); + } + + List _createBottomNavigationBarItems() { + return widget.pages + .map( + (page) => BottomNavigationBarItem( + icon: ButtomBarItemIcon( + iconData: bottomNavigationItems[page].icon, + hasBadge: page == Pages.cart, + badgeValue: page == Pages.cart ? widget.orderCount : 0, + ), + title: Text(bottomNavigationItems[page].title), + ), + ) + .toList(); + } +} diff --git a/ecommers/lib/ui/widgets/bottom_navigation/buttom_bar_item_icon.dart b/ecommers/lib/ui/widgets/bottom_navigation/buttom_bar_item_icon.dart new file mode 100644 index 0000000..a3dfd25 --- /dev/null +++ b/ecommers/lib/ui/widgets/bottom_navigation/buttom_bar_item_icon.dart @@ -0,0 +1,36 @@ +import 'package:badges/badges.dart'; +import 'package:ecommers/ui/decorations/dimens/insets.dart'; +import 'package:ecommers/ui/widgets/index.dart'; +import 'package:flutter/material.dart'; + +class ButtomBarItemIcon extends StatelessWidget { + final IconData iconData; + final bool hasBadge; + final int badgeValue; + + static const _iconWithBadgeWidth = 41.0; + + const ButtomBarItemIcon({ + @required this.iconData, + this.hasBadge = false, + this.badgeValue = 0, + }); + + @override + Widget build(BuildContext context) { + if (hasBadge) { + return Container( + alignment: Alignment.center, + width: _iconWithBadgeWidth, + child: IconWithBadge( + badgePosition: + BadgePosition.bottomLeft(bottom: Insets.x1, left: -Insets.x4), + badgeTextStyle: Theme.of(context).textTheme.overline, + icon: Icon(iconData), + badgeValue: badgeValue, + ), + ); + } + return Icon(iconData); + } +} diff --git a/ecommers/lib/ui/widgets/bottom_navigation/consts.dart b/ecommers/lib/ui/widgets/bottom_navigation/consts.dart new file mode 100644 index 0000000..34ac11d --- /dev/null +++ b/ecommers/lib/ui/widgets/bottom_navigation/consts.dart @@ -0,0 +1,11 @@ +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/ui/widgets/bottom_navigation/bottom_navigation_item_model.dart'; +import 'package:flutter/material.dart'; + +final Map bottomNavigationItems = { + Pages.home: BottomNavigationItemModel(icon: Icons.home, title: 'Home'), + Pages.search: BottomNavigationItemModel(icon: Icons.search, title: 'Search'), + Pages.cart: BottomNavigationItemModel(icon: Icons.shopping_cart, title: 'Cart'), + Pages.profile: BottomNavigationItemModel(icon: Icons.person_outline, title: 'Profile'), + Pages.more: BottomNavigationItemModel(icon: Icons.menu, title: 'More'), +}; diff --git a/ecommers/lib/ui/widgets/button/button_base_widget.dart b/ecommers/lib/ui/widgets/button/button_base_widget.dart new file mode 100644 index 0000000..8db3433 --- /dev/null +++ b/ecommers/lib/ui/widgets/button/button_base_widget.dart @@ -0,0 +1,80 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import '../../decorations/dimens/index.dart'; + +class ButtonBaseWidget extends StatelessWidget { + static const double _circleSize = 30.0; + static const double _iconHeight = 12.0; + + final Color buttonColor; + final Color textColor; + final Color iconBackgroundColor; + final Color blurColor; + final String text; + final String assetIcon; + final Function() onPressedFunction; + + const ButtonBaseWidget({ + @required this.text, + @required this.assetIcon, + @required this.buttonColor, + @required this.textColor, + @required this.blurColor, + @required this.onPressedFunction, + this.iconBackgroundColor, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + blurRadius: Radiuses.normal, + offset: const Offset(Insets.x0, Insets.x1_5), + color: blurColor, + ), + ], + borderRadius: BorderRadius.circular(Radiuses.big_2x), + ), + child: CupertinoButton( + padding: const EdgeInsets.all(Insets.x2), + borderRadius: BorderRadius.circular(Radiuses.big_2x), + color: buttonColor, + onPressed: onPressedFunction, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Align( + alignment: Alignment.center, + child: Text( + text.toUpperCase(), + style: Theme.of(context) + .textTheme + .button + .copyWith(color: textColor), + ), + ), + ), + Container( + alignment: Alignment.center, + height: _circleSize, + width: _circleSize, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: iconBackgroundColor ?? textColor, + ), + child: SvgPicture.asset( + assetIcon, + height: _iconHeight, + color: buttonColor, + ), + ) + ], + ), + ), + ); + } +} diff --git a/ecommers/lib/ui/widgets/button/index.dart b/ecommers/lib/ui/widgets/button/index.dart new file mode 100644 index 0000000..1321e3b --- /dev/null +++ b/ecommers/lib/ui/widgets/button/index.dart @@ -0,0 +1,3 @@ +export 'button_base_widget.dart'; +export 'primary_button_widget.dart'; +export 'secondary_button_widget.dart'; diff --git a/ecommers/lib/ui/widgets/button/primary_button_widget.dart b/ecommers/lib/ui/widgets/button/primary_button_widget.dart new file mode 100644 index 0000000..df36230 --- /dev/null +++ b/ecommers/lib/ui/widgets/button/primary_button_widget.dart @@ -0,0 +1,18 @@ +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/widgets/button/index.dart'; +import 'package:flutter/material.dart'; + +class PrimaryButtonWidget extends ButtonBaseWidget { + PrimaryButtonWidget({ + @required String text, + @required Function() onPressedFunction, + String assetIconPath = Assets.arrowRightIcon, + }) : super( + text: text, + assetIcon: assetIconPath, + buttonColor: BrandingColors.primary, + textColor: BrandingColors.secondaryText, + onPressedFunction: onPressedFunction, + blurColor: BrandingColors.primary.withOpacity(0.4), + ); +} diff --git a/ecommers/lib/ui/widgets/button/secondary_button_widget.dart b/ecommers/lib/ui/widgets/button/secondary_button_widget.dart new file mode 100644 index 0000000..71eb1f6 --- /dev/null +++ b/ecommers/lib/ui/widgets/button/secondary_button_widget.dart @@ -0,0 +1,18 @@ +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/widgets/button/index.dart'; +import 'package:flutter/material.dart'; + +class SecondaryButtonWidget extends ButtonBaseWidget { + const SecondaryButtonWidget({ + @required String text, + @required String assetIcon, + @required Function() onPressedFunction, + }) : super( + text: text, + assetIcon: assetIcon, + buttonColor: BrandingColors.background, + textColor: BrandingColors.secondary, + onPressedFunction: onPressedFunction, + blurColor: BrandingColors.secondary, + ); +} diff --git a/ecommers/lib/ui/widgets/category_item/categories_compact_widget.dart b/ecommers/lib/ui/widgets/category_item/categories_compact_widget.dart new file mode 100644 index 0000000..8ee5319 --- /dev/null +++ b/ecommers/lib/ui/widgets/category_item/categories_compact_widget.dart @@ -0,0 +1,92 @@ +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/core/services/index.dart'; +import 'package:ecommers/generated/i18n.dart'; +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/widgets/category_item/category_item.dart'; +import 'package:flutter/material.dart'; + +class CategoriesCompactWidget extends StatelessWidget { + static const _containerHeight = 134.0; + + static const categoryItemSize = Size(74.0, 89.0); + + @override + Widget build(BuildContext context) { + return Container( + height: _containerHeight, + padding: const EdgeInsets.symmetric(horizontal: Insets.x6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + I18n.of(context).categoriesTitle, + style: Theme.of(context).textTheme.headline6, + ), + ), + _createCategoriesListWidget(context), + ], + ), + ); + } + + Widget _createCategoriesListWidget(BuildContext context) { + final itemCount = _calculateItemCount(context); + final spacing = _calculateItemSpacing(context, itemCount); + + return SizedBox( + height: categoryItemSize.height, + child: ListView.separated( + itemCount: itemCount, + separatorBuilder: (BuildContext context, int index) => SizedBox( + width: spacing, + height: spacing, + ), + scrollDirection: Axis.horizontal, + itemBuilder: (BuildContext context, int index) { + if (index == itemCount - 1 && index != Categories.values.length - 1) { + return _buildSeeAllCategory(context); + } + + return CategoryItem.fromType(Categories.values[index]); + }, + ), + ); + } + + Widget _buildSeeAllCategory(BuildContext context) { + return CategoryItem( + backgroundColor: BrandingColors.background, + shadowColor: BrandingColors.blur, + imagePath: Assets.arrowRightIcon, + title: I18n.of(context).seeAllCategoryTitle, + onTapFunction: () => navigationService.navigateTo(Pages.categories), + ); + } + + int _calculateItemCount(BuildContext context) { + final categoriesListWidth = + MediaQuery.of(context).size.width - Dimens.pagePadding * 2; + + var itemCount = categoriesListWidth ~/ categoryItemSize.width; + + if (itemCount > Categories.values.length) { + itemCount = Categories.values.length; + } else { + itemCount = itemCount; + } + + return itemCount; + } + + double _calculateItemSpacing(BuildContext context, int itemCount) { + final categoriesListWidth = + MediaQuery.of(context).size.width - Dimens.pagePadding * 2; + + final calculatedListSpacing = + (categoriesListWidth % categoryItemSize.width) / (itemCount - 1); + + return calculatedListSpacing; + } +} diff --git a/ecommers/lib/ui/widgets/category_item/category_item.dart b/ecommers/lib/ui/widgets/category_item/category_item.dart new file mode 100644 index 0000000..7872aa2 --- /dev/null +++ b/ecommers/lib/ui/widgets/category_item/category_item.dart @@ -0,0 +1,88 @@ +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/ui/widgets/category_item/consts.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class CategoryItem extends StatelessWidget { + final Gradient labelBackgroundGradient; + final Color shadowColor; + final Color backgroundColor; + final String imagePath; + final String title; + final Function() onTapFunction; + + static const size = Size(74.0, 89.0); + static const categoryLabelSize = 65.0; + + const CategoryItem({ + @required this.shadowColor, + @required this.imagePath, + @required this.title, + this.labelBackgroundGradient, + this.backgroundColor, + this.onTapFunction, + }); + + factory CategoryItem.fromType(Categories categoryType) { + final categoryItem = categoryItems[categoryType]; + + return CategoryItem( + labelBackgroundGradient: categoryItem.gradient, + shadowColor: categoryItem.shadowColor, + imagePath: categoryItem.imagePath, + title: categoryItem.title, + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTapFunction, + child: Container( + width: size.width, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildGradientLabel(), + const SizedBox( + height: labelBottomMargin, + ), + Text( + title, + style: Theme.of(context) + .textTheme + .caption + .copyWith(fontSize: FontSizes.normal), + ), + ], + ), + ), + ); + } + + Widget _buildGradientLabel() { + return Container( + alignment: Alignment.center, + height: categoryLabelSize, + width: categoryLabelSize, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: backgroundColor, + boxShadow: [ + BoxShadow( + color: shadowColor, + blurRadius: Radiuses.big_1x, + offset: Dimens.defaultBlurOffset), + ], + gradient: labelBackgroundGradient, + ), + child: Center( + child: SvgPicture.asset( + imagePath, + fit: BoxFit.scaleDown, + ), + ), + ); + } +} diff --git a/ecommers/lib/ui/widgets/category_item/category_item_model.dart b/ecommers/lib/ui/widgets/category_item/category_item_model.dart new file mode 100644 index 0000000..b3714b6 --- /dev/null +++ b/ecommers/lib/ui/widgets/category_item/category_item_model.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class CategoryItemModel { + final Gradient gradient; + final Color shadowColor; + final String imagePath; + final String title; + + CategoryItemModel( + {this.gradient, + this.shadowColor, + this.imagePath, + this.title}); +} diff --git a/ecommers/lib/ui/widgets/category_item/consts.dart b/ecommers/lib/ui/widgets/category_item/consts.dart new file mode 100644 index 0000000..ff4513a --- /dev/null +++ b/ecommers/lib/ui/widgets/category_item/consts.dart @@ -0,0 +1,59 @@ +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:flutter/material.dart'; + +import 'category_item_model.dart'; + +const double labelBottomMargin = 5; +Color apparelShadowColor = const Color(0xffFF6262).withOpacity(0.34); +Color beautyShadowColor = const Color(0xff62AAFF).withOpacity(0.35); +Color shoesShadowColor = const Color(0xff26D555).withOpacity(0.34); +Color electronicsShadowColor = const Color(0xff7A71E8).withOpacity(0.34); +Color furnitureShadowColor = const Color(0xffD59F26).withOpacity(0.34); +Color homeShadowColor = const Color(0xff834162).withOpacity(0.34); +Color stationaryShadowColor = const Color(0xff646464).withOpacity(0.34); + +final Map categoryItems = { + Categories.apparel: CategoryItemModel( + gradient: Gradients.apparelCategory, + shadowColor: apparelShadowColor, + imagePath: Assets.apparelIcon, + title: 'Apparel', + ), + Categories.beauty: CategoryItemModel( + gradient: Gradients.beautyCategory, + shadowColor: beautyShadowColor, + imagePath: Assets.beautyIcon, + title: 'Apparel', + ), + Categories.shoes: CategoryItemModel( + gradient: Gradients.shoesCategory, + shadowColor: shoesShadowColor, + imagePath: Assets.shoesIcon, + title: 'Apparel', + ), + Categories.electronics: CategoryItemModel( + gradient: Gradients.electronicsCategory, + shadowColor: electronicsShadowColor, + imagePath: Assets.electronicsIcon, + title: 'Apparel', + ), + Categories.furniture: CategoryItemModel( + gradient: Gradients.furnitureCategory, + shadowColor: furnitureShadowColor, + imagePath: Assets.furnitureIcon, + title: 'Apparel', + ), + Categories.home: CategoryItemModel( + gradient: Gradients.homeCategory, + shadowColor: homeShadowColor, + imagePath: Assets.homeIcon, + title: 'Apparel', + ), + Categories.stationary: CategoryItemModel( + gradient: Gradients.stationaryCategory, + shadowColor: stationaryShadowColor, + imagePath: Assets.stationaryIcon, + title: 'Apparel', + ), +}; diff --git a/ecommers/lib/ui/widgets/circle_icon.dart b/ecommers/lib/ui/widgets/circle_icon.dart new file mode 100644 index 0000000..d4f99ae --- /dev/null +++ b/ecommers/lib/ui/widgets/circle_icon.dart @@ -0,0 +1,29 @@ +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class CircleIcon extends StatelessWidget { + static const size = Size(18.0, 18.0); + + final String imagePath; + + const CircleIcon({this.imagePath = Assets.arrowIcon}); + + @override + Widget build(BuildContext context) { + return Container( + width: size.width, + height: size.height, + alignment: Alignment.center, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: BrandingColors.backgroundIcon, + ), + child: SvgPicture.asset( + imagePath, + fit: BoxFit.scaleDown, + color: BrandingColors.secondary, + ), + ); + } +} diff --git a/ecommers/lib/ui/widgets/icon_with_badge.dart b/ecommers/lib/ui/widgets/icon_with_badge.dart new file mode 100644 index 0000000..6e7516b --- /dev/null +++ b/ecommers/lib/ui/widgets/icon_with_badge.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:badges/badges.dart'; +import '../decorations/dimens/index.dart'; +import '../decorations/index.dart'; + +class IconWithBadge extends Badge { + final int badgeValue; + final BadgePosition badgePosition; + final Widget icon; + final TextStyle badgeTextStyle; + + IconWithBadge({ + this.badgeValue = 0, + this.badgePosition, + this.badgeTextStyle, + @required this.icon, + }) : super( + child: icon, + badgeContent: Text( + badgeValue.toString(), + style: badgeTextStyle, + textAlign: TextAlign.center, + ), + position: + badgePosition ?? BadgePosition.bottomLeft(bottom: -4, left: -9), + shape: BadgeShape.square, + borderRadius: Radiuses.small_2x, + elevation: 1.0, + badgeColor: BrandingColors.primary, + animationType: BadgeAnimationType.scale, + showBadge: badgeValue != 0, + padding: const EdgeInsets.fromLTRB( + Insets.x1_5, + Insets.x0_5, + Insets.x1_5, + Insets.x0_5, + ), + ); +} diff --git a/ecommers/lib/ui/widgets/image_card.dart b/ecommers/lib/ui/widgets/image_card.dart new file mode 100644 index 0000000..6340c69 --- /dev/null +++ b/ecommers/lib/ui/widgets/image_card.dart @@ -0,0 +1,70 @@ +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/widgets/button/index.dart'; +import 'package:flutter/material.dart'; + +class ImageCard extends StatelessWidget { + static const Size _buttonSize = Size(120.0, 40.0); + + static const double _textWidth = 134.0; + static const int _textMaxLines = 3; + + static const double _borderRadius = 10.0; + static const imageCardSize = Size(325.0, 184.0); + + final String imageAsset; + final String description; + final String buttonText; + final Function() onButtonPressed; + + const ImageCard({ + this.imageAsset, + this.description, + this.buttonText, + this.onButtonPressed, + }); + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.centerLeft, + height: imageCardSize.height, + width: imageCardSize.width, + padding: const EdgeInsets.all(Insets.x6_5), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(imageAsset), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(_borderRadius), + ), + child: Column( + children: [ + Expanded( + child: SizedBox( + width: _textWidth, + child: Text( + description, + maxLines: _textMaxLines, + style: Theme.of(context).textTheme.headline3, + ), + ), + ), + + SizedBox( + height: _buttonSize.height, + width: _buttonSize.width, + child: ButtonBaseWidget( + text: buttonText, + assetIcon: Assets.arrowRightIcon, + buttonColor: BrandingColors.background, + textColor: BrandingColors.secondary, + onPressedFunction: onButtonPressed, + iconBackgroundColor: BrandingColors.primary, + blurColor: BrandingColors.secondary.withOpacity(0.15)), + ), + ], + ), + ); + } +} diff --git a/ecommers/lib/ui/widgets/index.dart b/ecommers/lib/ui/widgets/index.dart new file mode 100644 index 0000000..e4d236d --- /dev/null +++ b/ecommers/lib/ui/widgets/index.dart @@ -0,0 +1,5 @@ +export 'backgrounded_safe_area.dart'; +export 'circle_icon.dart'; +export 'icon_with_badge.dart'; +export 'image_card.dart'; +export 'rate_widget.dart'; diff --git a/ecommers/lib/ui/widgets/menu/index.dart b/ecommers/lib/ui/widgets/menu/index.dart new file mode 100644 index 0000000..469dad4 --- /dev/null +++ b/ecommers/lib/ui/widgets/menu/index.dart @@ -0,0 +1,3 @@ +export 'menu_item.dart'; +export 'menu_item_model.dart'; +export 'menu_list.dart'; diff --git a/ecommers/lib/ui/widgets/menu/menu_item.dart b/ecommers/lib/ui/widgets/menu/menu_item.dart new file mode 100644 index 0000000..405144d --- /dev/null +++ b/ecommers/lib/ui/widgets/menu/menu_item.dart @@ -0,0 +1,49 @@ +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class MenuItem extends StatelessWidget { + final String svgAssetIconPath; + final String title; + final String subTitle; + final double height; + + const MenuItem({ + this.svgAssetIconPath, + this.title, + this.subTitle, + this.height, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: height, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (svgAssetIconPath != null) + SizedBox( + width: 57.0, + child: SvgPicture.asset(svgAssetIconPath), + ) + else + const SizedBox(width: 15.0), + Expanded( + child: Text( + title ?? '', + style: Theme.of(context).textTheme.subtitle1, + ), + ), + Text( + subTitle ?? '', + style: Theme.of(context).textTheme.subtitle2, + ), + const SizedBox(width: 10.0), + SvgPicture.asset(Assets.menuArrowIcon), + const SizedBox(width: 15.0), + ], + ), + ); + } +} diff --git a/ecommers/lib/ui/widgets/menu/menu_item_model.dart b/ecommers/lib/ui/widgets/menu/menu_item_model.dart new file mode 100644 index 0000000..4e4bb10 --- /dev/null +++ b/ecommers/lib/ui/widgets/menu/menu_item_model.dart @@ -0,0 +1,11 @@ +class MenuItemModel { + final String svgAssetIconPath; + final String title; + final String subTitle; + + const MenuItemModel({ + this.svgAssetIconPath, + this.title, + this.subTitle + }); +} \ No newline at end of file diff --git a/ecommers/lib/ui/widgets/menu/menu_list.dart b/ecommers/lib/ui/widgets/menu/menu_list.dart new file mode 100644 index 0000000..3b63141 --- /dev/null +++ b/ecommers/lib/ui/widgets/menu/menu_list.dart @@ -0,0 +1,75 @@ +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/widgets/menu/index.dart'; +import 'package:flutter/material.dart'; + +class MenuList extends StatelessWidget { + final String title; + final List itemList; + final EdgeInsets margin; + final double itemHeight; + + const MenuList({ + this.title, + this.itemList, + this.margin = const EdgeInsets.all(0.0), + this.itemHeight = 48.0, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (title != null && title.isNotEmpty) + Padding( + padding: const EdgeInsets.only(bottom: Insets.x2_5), + child: Text( + title, + style: Theme.of(context).textTheme.headline5, + ), + ), + Container( + margin: margin, + decoration: BoxDecoration( + color: BrandingColors.background, + borderRadius: BorderRadius.circular(Radiuses.normal), + boxShadow: const [ + BoxShadow( + blurRadius: Radiuses.big_1x, + offset: Dimens.defaultBlurOffset, + color: BrandingColors.blur, + ) + ], + ), + child: ListView.separated( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: itemList.length, + padding: const EdgeInsets.all(0.0), + itemBuilder: (context, index) { + final itemModel = itemList[index]; + + return MenuItem( + title: itemModel.title, + subTitle: itemModel.subTitle, + svgAssetIconPath: itemModel.svgAssetIconPath, + height: itemHeight, + ); + }, + separatorBuilder: (context, index) { + final menuItem = itemList[index]; + + return Divider( + color: BrandingColors.secondary.withOpacity(0.1), + height: 1.0, + indent: menuItem.svgAssetIconPath == null ? 15.0 : 57.0, + endIndent: 17.0, + thickness: 1, + ); + }), + ), + ], + ); + } +} diff --git a/ecommers/lib/ui/widgets/order/circle_image.dart b/ecommers/lib/ui/widgets/order/circle_image.dart new file mode 100644 index 0000000..554db95 --- /dev/null +++ b/ecommers/lib/ui/widgets/order/circle_image.dart @@ -0,0 +1,29 @@ +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:flutter/material.dart'; + +class CircleImage extends StatelessWidget { + final Widget image; + final Size size; + + const CircleImage({ + @required this.image, + @required this.size, + }); + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.center, + height: size.height, + width: size.width, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: BrandingColors.background, + ), + child: Padding( + padding: const EdgeInsets.all(Insets.x2), + child: image, + ), + ); + } +} diff --git a/ecommers/lib/ui/widgets/order/counter.dart b/ecommers/lib/ui/widgets/order/counter.dart new file mode 100644 index 0000000..18cbb25 --- /dev/null +++ b/ecommers/lib/ui/widgets/order/counter.dart @@ -0,0 +1,70 @@ +import 'package:ecommers/ui/decorations/assets.dart'; +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/widgets/circle_icon.dart'; +import 'package:flutter/material.dart'; + +class Counter extends StatefulWidget { + final int count; + final Function() countIncrementFunction; + final Function() countDecrementFunction; + + const Counter({ + @required this.count, + @required this.countIncrementFunction, + @required this.countDecrementFunction, + }); + + @override + _CounterState createState() => _CounterState(); +} + +class _CounterState extends State { + static const _countRowWidth = 71.0; + @override + Widget build(BuildContext context) { + return SizedBox( + width: _countRowWidth, + child: Row( + children: [ + _buildCountActionButton( + Assets.substractIcon, + widget.countDecrementFunction, + ), + Expanded( + child: Align( + alignment: Alignment.center, + child: Text( + widget.count.toString(), + style: Theme.of(context) + .textTheme + .caption + .copyWith(fontSize: FontSizes.normal), + ), + ), + ), + _buildCountActionButton( + Assets.addIcon, + widget.countIncrementFunction, + ), + ], + ), + ); + } + + Widget _buildCountActionButton( + String imagePath, + Function() onTappedFunction, + ) { + return Container( + width: CircleIcon.size.width + Insets.x1, + height: CircleIcon.size.height + Insets.x1, + alignment: Alignment.center, + child: RawMaterialButton( + shape: const CircleBorder(), + onPressed: onTappedFunction, + elevation: 1.0, + child: CircleIcon(imagePath: imagePath), + ), + ); + } +} diff --git a/ecommers/lib/ui/widgets/order/index.dart b/ecommers/lib/ui/widgets/order/index.dart new file mode 100644 index 0000000..946594a --- /dev/null +++ b/ecommers/lib/ui/widgets/order/index.dart @@ -0,0 +1,5 @@ +export 'circle_image.dart'; +export 'counter.dart'; +export 'order_widget.dart'; +export 'small_order_widget.dart'; +export 'total_order_widget.dart'; diff --git a/ecommers/lib/ui/widgets/order/order_widget.dart b/ecommers/lib/ui/widgets/order/order_widget.dart new file mode 100644 index 0000000..ebc746c --- /dev/null +++ b/ecommers/lib/ui/widgets/order/order_widget.dart @@ -0,0 +1,86 @@ +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/utils/formatter.dart'; +import 'package:ecommers/ui/widgets/order/counter.dart'; +import 'package:ecommers/ui/widgets/order/index.dart'; +import 'package:flutter/material.dart'; + +class OrderWidget extends StatefulWidget { + final String assetImagePath; + final String primaryText; + final String secondaryText; + final double cost; + final int count; + final Function() countIncrementFunction; + final Function() countDecrementFunction; + + static const orderCircleImageSize = Size(80.0, 80.0); + + const OrderWidget({ + @required this.assetImagePath, + @required this.primaryText, + @required this.secondaryText, + @required this.cost, + @required this.count, + @required this.countIncrementFunction, + @required this.countDecrementFunction, + }); + + @override + _OrderWidgetState createState() => _OrderWidgetState(); +} + +class _OrderWidgetState extends State { + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleImage( + size: OrderWidget.orderCircleImageSize, + image: Image.asset( + widget.assetImagePath, + fit: BoxFit.scaleDown, + ), + ), + const SizedBox( + width: 20.0, + ), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.primaryText, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText1, + ), + Text( + widget.secondaryText, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText2, + ), + const SizedBox( + height: 8.0, + ), + Text( + Formatter.getCost(widget.count * widget.cost), + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(color: BrandingColors.primary), + ), + const SizedBox( + height: 8.0, + ), + Counter( + count: widget.count, + countIncrementFunction: widget.countIncrementFunction, + countDecrementFunction: widget.countDecrementFunction, + ) + ], + ), + ), + ], + ); + } +} diff --git a/ecommers/lib/ui/widgets/order/small_order_widget.dart b/ecommers/lib/ui/widgets/order/small_order_widget.dart new file mode 100644 index 0000000..9a6851e --- /dev/null +++ b/ecommers/lib/ui/widgets/order/small_order_widget.dart @@ -0,0 +1,86 @@ +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:ecommers/ui/utils/formatter.dart'; +import 'package:ecommers/ui/widgets/order/counter.dart'; +import 'package:ecommers/ui/widgets/order/index.dart'; +import 'package:flutter/material.dart'; + +class SmallOrderWidget extends StatefulWidget { + final String assetImagePath; + final String primaryText; + final String secondaryText; + final double cost; + final int count; + final Function() countIncrementFunction; + final Function() countDecrementFunction; + + static const orderCircleImageSize = Size(69.0, 69.0); + + const SmallOrderWidget({ + @required this.assetImagePath, + @required this.primaryText, + @required this.secondaryText, + @required this.cost, + @required this.count, + @required this.countIncrementFunction, + @required this.countDecrementFunction, + }); + + @override + _SmallOrderWidgetState createState() => _SmallOrderWidgetState(); +} + +class _SmallOrderWidgetState extends State { + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleImage( + size: SmallOrderWidget.orderCircleImageSize, + image: Image.asset( + widget.assetImagePath, + fit: BoxFit.scaleDown, + ), + ), + const SizedBox(width: Insets.x3_5), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.primaryText, + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(fontWeight: FontWeight.w700), + ), + Text( + widget.secondaryText, + style: Theme.of(context).textTheme.subtitle1, + ), + const SizedBox(height: Insets.x1), + Row( + children: [ + Expanded( + child: Text( + Formatter.getCost(widget.count * widget.cost), + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(color: BrandingColors.primary), + )), + Counter( + count: widget.count, + countIncrementFunction: widget.countIncrementFunction, + countDecrementFunction: widget.countDecrementFunction, + ), + ], + ), + ], + ), + ), + ], + ); + } +} diff --git a/ecommers/lib/ui/widgets/order/total_order_widget.dart b/ecommers/lib/ui/widgets/order/total_order_widget.dart new file mode 100644 index 0000000..0e93f67 --- /dev/null +++ b/ecommers/lib/ui/widgets/order/total_order_widget.dart @@ -0,0 +1,77 @@ +import 'package:ecommers/generated/i18n.dart'; +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/utils/formatter.dart'; +import 'package:ecommers/ui/widgets/button/index.dart'; +import 'package:flutter/material.dart'; + +class TotalOrderWidget extends StatelessWidget { + final String buttonText; + final double cost; + final Function() onButtonPressedFunction; + final Color backgroundColor; + final EdgeInsets padding; + + static const _buttonSize = Size(165.0, 46.0); + + const TotalOrderWidget({ + @required this.cost, + @required this.buttonText, + @required this.onButtonPressedFunction, + this.backgroundColor, + this.padding, + }); + + @override + Widget build(BuildContext context) { + return Container( + color: backgroundColor, + padding: padding, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + I18n.of(context).totalOrder, + style: Theme.of(context) + .textTheme + .headline5 + .copyWith(fontSize: FontSizes.small_1x), + ), + const SizedBox(height: 7.0), + Text( + Formatter.getCost(cost), + style: Theme.of(context) + .textTheme + .headline6 + .copyWith(fontSize: FontSizes.big_2x), + ), + const SizedBox(height: 4.0), + Text( + I18n.of(context).freeDomesticShipping, + style: Theme.of(context) + .textTheme + .headline5 + .copyWith(fontWeight: FontWeight.w400), + ), + ], + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: SizedBox( + height: _buttonSize.height, + width: _buttonSize.width, + child: PrimaryButtonWidget( + text: buttonText, + onPressedFunction: onButtonPressedFunction, + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/ecommers/lib/ui/widgets/product_item/index.dart b/ecommers/lib/ui/widgets/product_item/index.dart new file mode 100644 index 0000000..830bd9d --- /dev/null +++ b/ecommers/lib/ui/widgets/product_item/index.dart @@ -0,0 +1,4 @@ +export 'product_item_base.dart'; +export 'product_item_normal.dart'; +export 'product_item_small.dart'; +export 'product_item_wide.dart'; diff --git a/ecommers/lib/ui/widgets/product_item/product_item_base.dart b/ecommers/lib/ui/widgets/product_item/product_item_base.dart new file mode 100644 index 0000000..33af407 --- /dev/null +++ b/ecommers/lib/ui/widgets/product_item/product_item_base.dart @@ -0,0 +1,43 @@ +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:flutter/material.dart'; + +abstract class ProductItemBase extends StatelessWidget { + @protected + static const double padding = 10.0; + + final String assetImagePath; + final String title; + final double cost; + final Size productSize; + + const ProductItemBase({ + @required this.assetImagePath, + @required this.title, + @required this.cost, + @required this.productSize, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(10.0), + height: productSize.height, + width: productSize.width, + decoration: BoxDecoration( + color: BrandingColors.background, + borderRadius: BorderRadius.circular(Radiuses.normal), + boxShadow: const [ + BoxShadow( + blurRadius: Radiuses.big_1x, + color: BrandingColors.blur, + offset: Dimens.defaultBlurOffset, + ), + ], + ), + child: buildProductItem(context), + ); + } + + Widget buildProductItem(BuildContext context); +} diff --git a/ecommers/lib/ui/widgets/product_item/product_item_normal.dart b/ecommers/lib/ui/widgets/product_item/product_item_normal.dart new file mode 100644 index 0000000..ca3a2ce --- /dev/null +++ b/ecommers/lib/ui/widgets/product_item/product_item_normal.dart @@ -0,0 +1,54 @@ +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/utils/formatter.dart'; +import 'package:ecommers/ui/widgets/product_item/product_item_base.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class ProductItemNormal extends ProductItemBase { + final double rate; + + static const size = Size(101.0, 135.0); + + const ProductItemNormal({ + @required String assetImagePath, + @required String title, + @required double cost, + this.rate, + }) : super( + assetImagePath: assetImagePath, + cost: cost, + title: title, + productSize: size, + ); + + @override + Widget buildProductItem(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Center( + child: Image.asset(assetImagePath), + ), + ), + const SizedBox(height: 4.0), + Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: Dimens.defaultTextMaxLines, + style: Theme.of(context) + .textTheme + .bodyText2 + .copyWith(fontSize: FontSizes.small_3x), + ), + Text( + Formatter.getCost(cost), + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: FontSizes.small_1x, + fontWeight: FontWeight.w700, + ), + ), + ], + ); + } +} diff --git a/ecommers/lib/ui/widgets/product_item/product_item_small.dart b/ecommers/lib/ui/widgets/product_item/product_item_small.dart new file mode 100644 index 0000000..159b379 --- /dev/null +++ b/ecommers/lib/ui/widgets/product_item/product_item_small.dart @@ -0,0 +1,60 @@ +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/utils/formatter.dart'; +import 'package:ecommers/ui/widgets/product_item/product_item_base.dart'; +import 'package:flutter/material.dart'; + +class ProductItemSmall extends ProductItemBase { + final double rate; + + static const productItemSmallSize = Size(185.0, 59.0); + + const ProductItemSmall({ + @required String assetImagePath, + @required String title, + @required double cost, + this.rate, + }) : super( + assetImagePath: assetImagePath, + cost: cost, + title: title, + productSize: productItemSmallSize, + ); + + @override + Widget buildProductItem(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + flex: 3, + child: Image.asset(assetImagePath), + ), + const SizedBox( + width: ProductItemBase.padding, + ), + Expanded( + flex: 7, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: Dimens.defaultTextMaxLines, + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(fontWeight: FontWeight.w400), + ), + Text( + Formatter.getCost(cost), + style: Theme.of(context).textTheme.bodyText1, + ), + ], + ), + ) + ], + ); + } +} diff --git a/ecommers/lib/ui/widgets/product_item/product_item_wide.dart b/ecommers/lib/ui/widgets/product_item/product_item_wide.dart new file mode 100644 index 0000000..28c2a09 --- /dev/null +++ b/ecommers/lib/ui/widgets/product_item/product_item_wide.dart @@ -0,0 +1,58 @@ +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/utils/formatter.dart'; +import 'package:ecommers/ui/widgets/index.dart'; +import 'package:ecommers/ui/widgets/product_item/product_item_base.dart'; +import 'package:flutter/material.dart'; + +class ProductItemWide extends ProductItemBase { + final double rate; + + static const productItemWideSize = Size(160.0, 218.0); + + const ProductItemWide({ + @required String assetImagePath, + @required String title, + @required double cost, + this.rate, + }) : super( + assetImagePath: assetImagePath, + cost: cost, + title: title, + productSize: productItemWideSize, + ); + + @override + Widget buildProductItem(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Center( + child: Image.asset(assetImagePath), + ), + ), + Text( + title, + maxLines: Dimens.defaultTextMaxLines, + style: Theme.of(context).textTheme.bodyText2, + ), + Row( + children: [ + Expanded( + child: Text( + Formatter.getCost(cost), + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: FontSizes.small_3x, + fontWeight: FontWeight.w700, + ), + ), + ), + RateWidget( + rate: rate, + ), + ], + ) + ], + ); + } +} diff --git a/ecommers/lib/ui/widgets/progress.dart b/ecommers/lib/ui/widgets/progress.dart new file mode 100644 index 0000000..e1aaf05 --- /dev/null +++ b/ecommers/lib/ui/widgets/progress.dart @@ -0,0 +1,26 @@ +import 'dart:ui'; + +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:flare_flutter/flare_actor.dart'; +import 'package:flutter/material.dart'; + +class Progress extends StatelessWidget { + static const size = Size(70, 70); + static const String _animationState = '0to100'; + + const Progress({Key key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: SizedBox.fromSize( + size: Progress.size, + child: const FlareActor( + Assets.progressAnimation, + alignment: Alignment.center, + animation: _animationState, + ), + ), + ); + } +} diff --git a/ecommers/lib/ui/widgets/rate_widget.dart b/ecommers/lib/ui/widgets/rate_widget.dart new file mode 100644 index 0000000..4bcfd4f --- /dev/null +++ b/ecommers/lib/ui/widgets/rate_widget.dart @@ -0,0 +1,43 @@ +import 'package:ecommers/ui/decorations/dimens/index.dart'; +import 'package:ecommers/ui/decorations/index.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +class RateWidget extends StatelessWidget { + final Size size; + final double rate; + + static const rateContainerSize = Size(33.0, 16.0); + + const RateWidget({ + @required this.rate, + this.size, + }); + + @override + Widget build(BuildContext context) { + final containerSize = size ?? rateContainerSize; + + return Container( + height: containerSize.height, + width: containerSize.width, + alignment: Alignment.center, + decoration: BoxDecoration( + color: BrandingColors.primary, + borderRadius: BorderRadius.circular(Radiuses.big_1x), + ), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset(Assets.rateStarIcon), + Text( + rate.toString(), + style: Theme.of(context).textTheme.overline, + ) + ], + ), + ), + ); + } +} diff --git a/ecommers/lib/web_server/data_access/user_data_access.dart b/ecommers/lib/web_server/data_access/user_data_access.dart new file mode 100644 index 0000000..90be26c --- /dev/null +++ b/ecommers/lib/web_server/data_access/user_data_access.dart @@ -0,0 +1,74 @@ +import 'package:ecommers/web_server/data_access/validation_model.dart'; +import 'package:ecommers/web_server/local_database.dart'; +import 'package:ecommers/web_server/models/user.dart'; +import 'package:ecommers/extensions/string_extension.dart'; + +class UserDataAccess { + static const String userStoreKey = 'Users'; + + static final UserDataAccess _instance = UserDataAccess._(); + static UserDataAccess get instance => _instance; + + final LocalDatabase _database = LocalDatabase.instance; + + UserDataAccess._(); + + List _allUsers; + Future> get allUsers => _getAllUsers(); + + Future> _getAllUsers() async { + return _allUsers ??= + await _database.getAll(userStoreKey, User.fromJsonFactory); + } + + Future saveUser(Map userMap) async { + _allUsers ??= []; + _allUsers.add(User.fromJsonFactory(userMap)); + + await _database.saveMap(userStoreKey, userMap); + } + + Future isUserExists(User currentUser) async { + final existedUser = (await allUsers).firstWhere( + (user) => + (user.email == currentUser.email || + user.username == currentUser.username) && + user.password == currentUser.password, + orElse: () => null); + + return existedUser != null; + } + + Future isNewUserValid(User newUser) async { + const fieldsEmptyError = 'Please fill in all fields'; + const emailExistsError = 'This email already exists'; + const usernameExists = 'This username already exists'; + + final users = await allUsers; + + if (newUser.email.isNullOrEmpty || + newUser.username.isNullOrEmpty || + newUser.password.isNullOrEmpty) { + return ValidationModel(isValid: false, error: fieldsEmptyError); + } + + if (users == null) { + return ValidationModel(isValid: true); + } + + final existedUser = (await allUsers).firstWhere( + (user) => + user.email == newUser.email || user.username == newUser.username, + orElse: () => null); + + if (existedUser == null) { + return ValidationModel(isValid: true); + } + + if (existedUser.email == newUser.email) { + return ValidationModel(isValid: false, error: emailExistsError); + } + + return ValidationModel(isValid: false, error: usernameExists); + } +} diff --git a/ecommers/lib/web_server/data_access/validation_model.dart b/ecommers/lib/web_server/data_access/validation_model.dart new file mode 100644 index 0000000..58638f9 --- /dev/null +++ b/ecommers/lib/web_server/data_access/validation_model.dart @@ -0,0 +1,6 @@ +class ValidationModel { + final String error; + final bool isValid; + + ValidationModel({this.error = '',this.isValid}); +} \ No newline at end of file diff --git a/ecommers/lib/web_server/local_database.dart b/ecommers/lib/web_server/local_database.dart new file mode 100644 index 0000000..07db841 --- /dev/null +++ b/ecommers/lib/web_server/local_database.dart @@ -0,0 +1,59 @@ +import 'dart:async'; + +import 'package:path_provider/path_provider.dart'; +import 'package:sembast/sembast.dart'; +import 'package:sembast/sembast_io.dart'; +import 'package:path/path.dart'; + +class LocalDatabase { + static final LocalDatabase _instance = LocalDatabase._(); + static LocalDatabase get instance => _instance; + + Completer _dbCompleter; + + Future get _db async { + if (_dbCompleter == null) { + _dbCompleter = Completer(); + + await initializeDatabase(); + } + + return _dbCompleter.future; + } + + LocalDatabase._(); + + Future initializeDatabase() async { + final DatabaseFactory dbFactory = databaseFactoryIo; + + final database = await dbFactory.openDatabase(await _getDbPath()); + + _dbCompleter.complete(database); + } + + Future _getDbPath() async { + const _dbName = 'local_server.db'; + + final dbDirectory = await getApplicationDocumentsDirectory(); + if (!dbDirectory.existsSync()) { + await dbDirectory.create(recursive: true); + } + + return join(dbDirectory.path, _dbName); + } + + Future saveMap(String key, Map map) async { + final store = intMapStoreFactory.store(key); + await store.add(await _db, map); + } + + Future> getAll( + String key, T Function(Map) fromMap) async { + final store = intMapStoreFactory.store(key); + final records = await store.find(await _db); + + return records.map((snapshot) { + return fromMap(snapshot.value); + }).toList(); + } +} diff --git a/ecommers/lib/web_server/local_server.dart b/ecommers/lib/web_server/local_server.dart new file mode 100644 index 0000000..6d9368d --- /dev/null +++ b/ecommers/lib/web_server/local_server.dart @@ -0,0 +1,34 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:ecommers/core/services/index.dart'; +import 'package:http_server/http_server.dart'; + +class LocalServer { + static const int _port = 8090; + static final String _localHost = InternetAddress.loopbackIPv4.address; + static HttpServer _server; + + static Uri uri = Uri(scheme: 'http', host: _localHost, port: _port); + + static Future setup() async { + _server = await HttpServer.bind( + _localHost, + _port, + shared: true, + ); + + _server.transform(HttpBodyHandler()).listen((HttpRequestBody body) async { + await Future.delayed(const Duration(seconds: 2)); + requestHandler.process(body); + }); + } + + static void closeConnection() { + if (_server == null) { + return; + } + + _server.close(); + } +} diff --git a/ecommers/lib/web_server/models/user.dart b/ecommers/lib/web_server/models/user.dart new file mode 100644 index 0000000..8069488 --- /dev/null +++ b/ecommers/lib/web_server/models/user.dart @@ -0,0 +1,21 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'user.g.dart'; + +@JsonSerializable() +class User { + @JsonKey(name: 'username') + final String username; + + @JsonKey(name: 'email') + final String email; + + @JsonKey(name: 'password') + final String password; + + User(this.username, this.email, this.password); + + static const fromJsonFactory = _$UserFromJson; + + Map toJson() => _$UserToJson(this); +} \ No newline at end of file diff --git a/ecommers/lib/web_server/request_handler.dart b/ecommers/lib/web_server/request_handler.dart new file mode 100644 index 0000000..b6ef8be --- /dev/null +++ b/ecommers/lib/web_server/request_handler.dart @@ -0,0 +1,82 @@ +import 'dart:io'; + +import 'package:ecommers/core/common/index.dart'; +import 'package:ecommers/core/services/index.dart'; +import 'package:ecommers/web_server/data_access/user_data_access.dart'; +import 'package:http_server/http_server.dart'; + +import 'models/user.dart'; + +class RequestHandler { + static final UserDataAccess _userDataAccess = UserDataAccess.instance; + + void process(HttpRequestBody body) { + final uri = body.request.uri.toString(); + + switch (uri) { + case ApiDefines.login: + _handleLoginRequest(body); + break; + case ApiDefines.auth: + _handleAuthorizationRequest(body); + break; + default: + _handleUnsupportedRequest(body); + } + } + + Future _handleLoginRequest(HttpRequestBody body) async { + const String jsonFile = 'login.json'; + + final userMap = body.body as Map; + final user = User.fromJsonFactory(userMap); + + if (await _userDataAccess.isUserExists(user)) { + body.request.response + ..headers.contentType = ContentType.json + ..write(await fileManager.readJson(jsonFile)) + ..close(); + + return; + } + + body.request.response + ..statusCode = HttpStatus.unauthorized + ..close(); + } + + Future _handleAuthorizationRequest(HttpRequestBody body) async { + const String jsonFile = 'login.json'; + + final userMap = body.body as Map; + final user = User.fromJsonFactory(userMap); + + final validationModel = await _userDataAccess.isNewUserValid(user); + + if (validationModel.isValid) { + await _userDataAccess.saveUser(userMap); + + body.request.response + ..headers.contentType = ContentType.json + ..write(await fileManager.readJson(jsonFile)) + ..close(); + + return; + } + + body.request.response + ..statusCode = HttpStatus.unauthorized + ..write(validationModel.error) + ..close(); + } + + Future _handleUnsupportedRequest(HttpRequestBody body) async { + const String unsupported = 'Unsupported API'; + + body.request.response + ..headers.contentType = ContentType.text + ..statusCode = HttpStatus.notImplemented + ..write(unsupported) + ..close(); + } +} diff --git a/ecommers/pubspec.yaml b/ecommers/pubspec.yaml index 1a9b430..318c985 100644 --- a/ecommers/pubspec.yaml +++ b/ecommers/pubspec.yaml @@ -14,27 +14,55 @@ description: A new Flutter project. version: 1.0.0+1 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.6.0 <3.0.0" dependencies: flutter: sdk: flutter + flutter_svg: ^0.17.2 + intl: ^0.16.1 + carousel_slider: ^1.4.1 + provider: ^4.0.4 + badges: ^1.1.1 + lint: ^1.1.1 + get_it: ^4.0.0 + keyboard_visibility: ^0.5.6 + flutter_launcher_icons: ^0.7.4 + http_server: ^0.9.8+3 + chopper: ^3.0.2 + json_annotation: ^3.0.1 + flutter_secure_storage: ^3.3.1+1 + flare_flutter: ^2.0.1 + sembast: ^2.3.0 + path_provider: ^1.6.5 + +flutter_icons: + image_path_android: "assets/launch_icon_android.png" #http://www.softicons.com/web-icons/services-flat-icons-by-jozef-krajcovic/e-commerce-icon + image_path_ios: "assets/launch_icon_ios.png" + android: true + ios: true # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 + cupertino_icons: ^0.1.3 dev_dependencies: flutter_test: sdk: flutter + chopper_generator: ^3.0.4 + json_serializable: ^3.2.5 + build_runner: ^1.8.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: - + assets: + - assets/ + - assets/data/ + # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. diff --git a/ecommers/test/widget_test.dart b/ecommers/test/widget_test.dart index 60afd84..4732d30 100644 --- a/ecommers/test/widget_test.dart +++ b/ecommers/test/widget_test.dart @@ -11,9 +11,9 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:ecommers/main.dart'; void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { + testWidgets('Counter increments smoke test', (tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); + await tester.pumpWidget(MainApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget); diff --git a/xd-resources-ecommerce-ui.xd b/xd-resources-ecommerce-ui.xd new file mode 100644 index 0000000..49a384f Binary files /dev/null and b/xd-resources-ecommerce-ui.xd differ