diff --git a/README.md b/README.md index 36ba0bb..11e6a06 100644 --- a/README.md +++ b/README.md @@ -32,3 +32,17 @@ - Using Google Firebase products, Cloud Firestore, Cloud Authentication, to maintain backend easily. - Implementing Mobile Security measures to demonstrate how a mobile app should be secured. +# API Reference +- [Abstract API](https://www.abstractapi.com/) +# Setup +- Follow the steps accordingly. +## Version +- Make sure your IDE has the latest Flutter, Kotlin, Dart versions. +- Also make sure your Android SDK version is the latest. +## Firebase +- Navigate to [Firebase](https://firebase.google.com/) and create a Firebase project and update your config files including google-services.json +## Pubspec.yaml +- You have to include the packages in [pubspec.yaml]() +## Mobile Security +### .env +- You need to create a [.env]() file to secure your api keys. diff --git a/assets/fonts/Exo2-Italic-VariableFont_wght.ttf b/assets/fonts/Exo2-Italic-VariableFont_wght.ttf new file mode 100644 index 0000000..fd7f356 Binary files /dev/null and b/assets/fonts/Exo2-Italic-VariableFont_wght.ttf differ diff --git a/assets/fonts/Exo2-VariableFont_wght.ttf b/assets/fonts/Exo2-VariableFont_wght.ttf new file mode 100644 index 0000000..d06bdc2 Binary files /dev/null and b/assets/fonts/Exo2-VariableFont_wght.ttf differ diff --git a/assets/fonts/LICENSE.txt b/assets/fonts/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/assets/fonts/LICENSE.txt @@ -0,0 +1,202 @@ + + 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 [yyyy] [name of copyright owner] + + 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/assets/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf b/assets/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000..0fea34b Binary files /dev/null and b/assets/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf differ diff --git a/assets/fonts/OpenSans-VariableFont_wdth,wght.ttf b/assets/fonts/OpenSans-VariableFont_wdth,wght.ttf new file mode 100644 index 0000000..51dd3c3 Binary files /dev/null and b/assets/fonts/OpenSans-VariableFont_wdth,wght.ttf differ diff --git a/assets/fonts/README.txt b/assets/fonts/README.txt new file mode 100644 index 0000000..6a21f11 --- /dev/null +++ b/assets/fonts/README.txt @@ -0,0 +1,100 @@ +Open Sans Variable Font +======================= + +This download contains Open Sans as both variable fonts and static fonts. + +Open Sans is a variable font with these axes: + wdth + wght + +This means all the styles are contained in these files: + OpenSans-VariableFont_wdth,wght.ttf + OpenSans-Italic-VariableFont_wdth,wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Open Sans: + static/OpenSans_Condensed/OpenSans_Condensed-Light.ttf + static/OpenSans_Condensed/OpenSans_Condensed-Regular.ttf + static/OpenSans_Condensed/OpenSans_Condensed-Medium.ttf + static/OpenSans_Condensed/OpenSans_Condensed-SemiBold.ttf + static/OpenSans_Condensed/OpenSans_Condensed-Bold.ttf + static/OpenSans_Condensed/OpenSans_Condensed-ExtraBold.ttf + static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-Light.ttf + static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-Regular.ttf + static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-Medium.ttf + static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-SemiBold.ttf + static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-Bold.ttf + static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-ExtraBold.ttf + static/OpenSans/OpenSans-Light.ttf + static/OpenSans/OpenSans-Regular.ttf + static/OpenSans/OpenSans-Medium.ttf + static/OpenSans/OpenSans-SemiBold.ttf + static/OpenSans/OpenSans-Bold.ttf + static/OpenSans/OpenSans-ExtraBold.ttf + static/OpenSans_Condensed/OpenSans_Condensed-LightItalic.ttf + static/OpenSans_Condensed/OpenSans_Condensed-Italic.ttf + static/OpenSans_Condensed/OpenSans_Condensed-MediumItalic.ttf + static/OpenSans_Condensed/OpenSans_Condensed-SemiBoldItalic.ttf + static/OpenSans_Condensed/OpenSans_Condensed-BoldItalic.ttf + static/OpenSans_Condensed/OpenSans_Condensed-ExtraBoldItalic.ttf + static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-LightItalic.ttf + static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-Italic.ttf + static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-MediumItalic.ttf + static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-SemiBoldItalic.ttf + static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-BoldItalic.ttf + static/OpenSans_SemiCondensed/OpenSans_SemiCondensed-ExtraBoldItalic.ttf + static/OpenSans/OpenSans-LightItalic.ttf + static/OpenSans/OpenSans-Italic.ttf + static/OpenSans/OpenSans-MediumItalic.ttf + static/OpenSans/OpenSans-SemiBoldItalic.ttf + static/OpenSans/OpenSans-BoldItalic.ttf + static/OpenSans/OpenSans-ExtraBoldItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (LICENSE.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them freely in your products & projects - print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/assets/images/icon.png b/assets/images/icon.png new file mode 100644 index 0000000..38c8aa1 Binary files /dev/null and b/assets/images/icon.png differ diff --git a/assets/images/splash_screen.png b/assets/images/splash_screen.png new file mode 100644 index 0000000..50f207b Binary files /dev/null and b/assets/images/splash_screen.png differ diff --git a/img/env-file.png b/img/env-file.png new file mode 100644 index 0000000..da5ceb2 Binary files /dev/null and b/img/env-file.png differ diff --git a/img/firestore-0.png b/img/firestore-0.png new file mode 100644 index 0000000..0a948bb Binary files /dev/null and b/img/firestore-0.png differ diff --git a/lib/helpers/db_helper.dart b/lib/helpers/db_helper.dart new file mode 100644 index 0000000..a66e4a2 --- /dev/null +++ b/lib/helpers/db_helper.dart @@ -0,0 +1,42 @@ +import 'package:sqflite/sqflite.dart' as sql; +import 'package:path/path.dart' as path; +import 'package:sqflite/sqlite_api.dart'; + +class DBHelper { + static Future database() async { + final dbPath = await sql.getDatabasesPath(); + return sql.openDatabase(path.join(dbPath, 'cart.db'), + onCreate: (db, version) { + return db.execute( + 'CREATE TABLE cart_items(id TEXT PRIMARY KEY, title TEXT, price TEXT, imageUrl TEXT)'); + }, version: 1); + } + + static Future insert(String table, Map data) async { + final db = await DBHelper.database(); + db.insert( + table, + data, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + static Future>> getData(String table) async { + final db = await DBHelper.database(); + return db.query(table); + } + + static Future deleteItem(String id) async { + final db = await DBHelper.database(); + return await db.delete( + "cart_items", + where: 'id = ?', + whereArgs: [id], + ); + } + + static Future deleteAllItems() async { + final db = await DBHelper.database(); + return await db.delete("cart_items"); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..e69dcb5 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,191 @@ +import 'package:agu_store_flutter/pages/accountpage.dart'; +import 'package:agu_store_flutter/pages/cartpage.dart'; +import 'package:agu_store_flutter/pages/homepage.dart'; +import 'package:agu_store_flutter/pages/settingspage.dart'; +import 'package:agu_store_flutter/providers/cart_provider.dart'; +import 'package:agu_store_flutter/providers/email_signin_provider.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:overlay_support/overlay_support.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter/material.dart'; +import 'package:agu_store_flutter/utils/constants.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'dart:developer' as dev; + +const AndroidNotificationChannel channel = AndroidNotificationChannel( + 'high_importance_channel', + 'High Importance Notifications', + description: 'This channel is used for important notifications.', + importance: Importance.high, + playSound: true, +); + +final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + +Future _firebaseMessagingBackgroundHandler(RemoteMessage msg) async { + await Firebase.initializeApp(); + dev.log("A background message has been received: ${msg.messageId}"); +} + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(); + FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.createNotificationChannel(channel); + await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions( + alert: true, + badge: true, + sound: true, + ); + await dotenv.load(fileName: ".env"); + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => EmailSignInProvider(), + ), + ChangeNotifierProvider( + create: (context) => CartProvider(), + ), + ], + child: OverlaySupport( + child: MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData( + fontFamily: kFontFamily1, + ), + home: const RootPage(), + ), + ), + ), + ); +} + +class RootPage extends StatefulWidget { + const RootPage({Key? key}) : super(key: key); + + @override + State createState() => RootPageState(); +} + +class RootPageState extends State { + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: kRootAppBackground, + appBar: getTopAppBar(), + bottomNavigationBar: getBottomAppBar(), + body: IndexedStack( + index: activePageIndex, + children: [ + HomePage(), + ChangeNotifierProvider.value( + value: CartProvider(), child: CartPage()), + AccountPage(), + SettingsPage(), + ], + ), + ); + } + + // Reference to the active page index + int activePageIndex = 0; + late AppBar? appBar; + + // So, the top app bar can be easily customized by the pages + PreferredSizeWidget? getTopAppBar() { + switch (activePageIndex) { + case 0: + return appBar = null; + case 1: + return AppBar( + elevation: 0.3, + backgroundColor: kTopAppBarBackgroundColor, + title: const Text( + "CART", + style: TextStyle( + color: Colors.black, + fontFamily: kFontFamily2, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: true, + ); + case 2: + return AppBar( + elevation: 0.3, + backgroundColor: kTopAppBarBackgroundColor, + title: const Text( + "ACCOUNT", + style: TextStyle( + color: Colors.black, + fontFamily: kFontFamily2, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: true, + ); + case 3: + return AppBar( + elevation: 0.3, + backgroundColor: kTopAppBarBackgroundColor, + title: const Text( + "SETTINGS", + style: TextStyle( + color: Colors.black, + fontFamily: kFontFamily2, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: true, + ); + default: + } + } + + Widget getBottomAppBar() { + return Container( + height: 64, + decoration: BoxDecoration( + color: kBottomAppbarBackgroundColor, + border: Border( + top: BorderSide( + color: Colors.grey.withOpacity(0.2), + ), + ), + ), + child: Padding( + padding: const EdgeInsets.only(left: 10, right: 10, top: 10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate( + kItemsTab.length, + (index) { + return IconButton( + icon: Icon( + kItemsTab[index]['icon'], + size: kItemsTab[index]['size'], + color: activePageIndex == index + ? kBottomAppbarIconsAccentColor + : kBottomAppbarIconsColor, + ), + onPressed: () { + setState(() { + activePageIndex = index; + }); + }); + }, + ), + ), + ), + ); + } +} diff --git a/lib/models/category.dart b/lib/models/category.dart new file mode 100644 index 0000000..9b75aab --- /dev/null +++ b/lib/models/category.dart @@ -0,0 +1,11 @@ +class Category { + final String id; + final String label; + final String img; + + Category({ + required this.id, + required this.label, + required this.img, + }); +} diff --git a/lib/models/product.dart b/lib/models/product.dart new file mode 100644 index 0000000..e5bc644 --- /dev/null +++ b/lib/models/product.dart @@ -0,0 +1,16 @@ +class Product +{ + final String id; + final String title; + final String description; + final String price; + final String imageUrl; + + Product({ + required this.id, + required this.title, + required this.description, + required this.price, + required this.imageUrl, + }); +} \ No newline at end of file diff --git a/lib/pages/accountpage.dart b/lib/pages/accountpage.dart new file mode 100644 index 0000000..24851a0 --- /dev/null +++ b/lib/pages/accountpage.dart @@ -0,0 +1,38 @@ +import 'package:agu_store_flutter/screens/profile_screen.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import '../screens/auth_screen.dart'; + +class AccountPage extends StatefulWidget { + const AccountPage({Key? key}) : super(key: key); + + @override + _AccountPageState createState() => _AccountPageState(); +} + +class _AccountPageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: StreamBuilder( + stream: FirebaseAuth.instance.authStateChanges(), + builder: (_, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (snapshot.hasData) { + return const ProfileScreen(); + } else if (snapshot.hasError) { + return const Center( + child: Text("Something Went Wrong!"), + ); + } else { + return const AuthScreen(); + } + }, + ), + ); + } +} diff --git a/lib/pages/cartpage.dart b/lib/pages/cartpage.dart new file mode 100644 index 0000000..ba6127d --- /dev/null +++ b/lib/pages/cartpage.dart @@ -0,0 +1,185 @@ +import 'package:agu_store_flutter/helpers/db_helper.dart'; +import 'package:agu_store_flutter/providers/cart_provider.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../main.dart'; +import '../utils/constants.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'dart:developer' as dev; + +class CartPage extends StatefulWidget { + const CartPage({Key? key}) : super(key: key); + + @override + _CartPageState createState() => _CartPageState(); +} + +class _CartPageState extends State { + @override + void initState() { + super.initState(); + FirebaseMessaging.onMessage.listen( + (RemoteMessage message) { + RemoteNotification? notification = message.notification; + AndroidNotification? android = message.notification?.android; + if (notification != null && android != null) { + flutterLocalNotificationsPlugin.show( + notification.hashCode, + notification.title, + notification.body, + NotificationDetails( + android: AndroidNotificationDetails( + channel.id, + channel.name, + channelDescription: channel.description, + color: Colors.blue, + playSound: true, + icon: '@mipmap/ic_launcher', + ), + ), + ); + } + }, + ); + } + + int getTotalPrice() { + return Provider.of(context, listen: false).getTotalPrice(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: kCartPageBackground, + body: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: FutureBuilder( + future: Provider.of(context, listen: false) + .fetchAndSetItems(), + builder: (ctx, snapshot) => snapshot.connectionState == + ConnectionState.waiting + ? const Center( + child: CircularProgressIndicator(), + ) + : Consumer( + builder: (ctx, cartProviderItem, ch) { + return Column( + children: [ + ListView.builder( + scrollDirection: Axis.vertical, + shrinkWrap: true, + itemCount: cartProviderItem.items.length, + itemBuilder: (ctx, index) { + final item = cartProviderItem.items[index]; + return Dismissible( + key: Key(item.id), + background: Container(color: Colors.red), + onDismissed: (direction) { + // Remove the item from the data source. + setState( + () { + dev.log("Performing removing " + item.title); + DBHelper.deleteItem(item.id); + }, + ); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('${item.title} removed'))); + }, + child: ListTile( + leading: Container( + height: 200, + width: 100, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + image: DecorationImage( + fit: BoxFit.cover, + image: NetworkImage( + cartProviderItem + .items[index].imageUrl, + ), + ), + ), + ), + subtitle: Text( + cartProviderItem.items[index].price + " \$", + style: const TextStyle( + fontSize: 18, + fontFamily: kFontFamily2, + ), + ), + title: Text( + cartProviderItem.items[index].title, + style: const TextStyle( + fontSize: 18, + fontFamily: kFontFamily2, + ), + ), + ), + ); + }), + const SizedBox( + height: 10, + ), + Text( + "Total Price: ${getTotalPrice()} \$", + style: const TextStyle( + fontSize: 22, + fontFamily: kFontFamily2, + fontWeight: FontWeight.bold, + ), + ), + ], + ); + }, + ), + ), + ), + persistentFooterButtons: [ + ElevatedButton( + onPressed: () { + dev.log("Performing a schedule notification!"); + flutterLocalNotificationsPlugin.show( + 0, //do not change this value + "AGU STORE", + "WE GOT YOUR ORDER", + NotificationDetails( + android: AndroidNotificationDetails(channel.id, channel.name, + channelDescription: channel.description, + importance: Importance.high, + color: Colors.blue, + playSound: true, + styleInformation: const BigPictureStyleInformation( + DrawableResourceAndroidBitmap("@mipmap/ic_launcher"), + largeIcon: + DrawableResourceAndroidBitmap("@mipmap/ic_launcher"), + htmlFormatContent: true, + htmlFormatContentTitle: true, + ), + icon: '@mipmap/ic_launcher'), + ), + ); + DBHelper.deleteAllItems(); + }, + child: const Text("Order"), + style: ElevatedButton.styleFrom( + primary: kMainTheme, + fixedSize: const Size(340, 50), + shape: const StadiumBorder(), + textStyle: const TextStyle( + fontFamily: kFontFamily2, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox( + width: 10, + ) + ], + ); + } +} diff --git a/lib/pages/homepage.dart b/lib/pages/homepage.dart new file mode 100644 index 0000000..081d93a --- /dev/null +++ b/lib/pages/homepage.dart @@ -0,0 +1,398 @@ +import 'dart:async'; + +import 'package:agu_store_flutter/screens/products_overview.dart'; +import 'package:flutter/material.dart'; +import 'package:agu_store_flutter/utils/constants.dart'; +import '../screens/category_overview.dart'; +import '../services/product_db_connector.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/services.dart'; +import 'dart:developer' as dev; + +class HomePage extends StatefulWidget { + const HomePage({Key? key}) : super(key: key); + + @override + _HomePageState createState() => _HomePageState(); +} + +class _HomePageState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: getHomePageBody(), + ); + } + + ConnectivityResult _connectionStatus = ConnectivityResult.none; + final Connectivity _connectivity = Connectivity(); + late StreamSubscription _connectivitySubscription; + + @override + void initState() { + super.initState(); + initConnectivity(); + + _connectivitySubscription = + _connectivity.onConnectivityChanged.listen(_updateConnectionStatus); + } + + @override + void dispose() { + _connectivitySubscription.cancel(); + super.dispose(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initConnectivity() async { + late ConnectivityResult result; + // Platform messages may fail, so we use a try/catch PlatformException. + try { + result = await _connectivity.checkConnectivity(); + } on PlatformException catch (e) { + dev.log('Couldn\'t check connectivity status', error: e); + return; + } + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) { + return Future.value(null); + } + + return _updateConnectionStatus(result); + } + + Future _updateConnectionStatus(ConnectivityResult result) async { + setState(() { + _connectionStatus = result; + }); + } + + final dbCategories = ProductDBConnector(); + + Widget getHomePageBody() { + var size = MediaQuery.of(context).size; + return ListView( + padding: EdgeInsets.zero, + children: [ + Stack( + children: [ + Container( + width: size.width, + height: kHomeImgHeight, + decoration: const BoxDecoration( + image: DecorationImage( + image: NetworkImage(kHomeImg), + fit: BoxFit.cover, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 35, right: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const SizedBox( + width: 15, + ), + IconButton( + onPressed: () { + dev.log("request to search"); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Connection Status: ${_connectionStatus.toString()}'), + ), + ); + }, + icon: const Icon(kSearchButtonIcon), + iconSize: kTopAppBarIconsSize, + color: kTopAppBarIconsColor, + hoverColor: Colors.black, + ), + ], + ), + ), + Positioned( + bottom: 20, + child: Padding( + padding: const EdgeInsets.only(left: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + TextButton( + onPressed: () { + dev.log( + "Request to get agu_special collection data"); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ProductsOverviewScreen("agu_special")), + ); + }, + child: const Text( + "Discover $kHomePageImgCategory", + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: kHomePageImgTitleSize, + color: kHomePageImgTitleColor, + fontFamily: kFontFamily1, + fontWeight: FontWeight.bold), + ), + ), + const Icon( + kForwardSign, + color: kHomePageImgTitleColor, + size: kHomePageImgTitleSize, + ), + ], + ), + ], + ), + ), + ), + ], + ), + const SizedBox( + height: 40, + ), + Padding( + padding: const EdgeInsets.only(left: 15, right: 15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Categories", + style: TextStyle( + fontSize: kCategoriesFontSize, + fontFamily: kFontFamily2, + fontWeight: FontWeight.bold), + ), + Row( + children: [ + TextButton( + onPressed: () { + dev.log("User wants to see all categories"); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CategoryOverviewScreen()), + ); + }, + child: const Text( + "See all", + style: TextStyle( + color: Colors.grey, + fontSize: kCategoriesFontSize, + fontFamily: kFontFamily2, + ), + ), + ), + const SizedBox( + width: 5, + ), + const Icon( + kForwardSign, + color: Colors.grey, + ) + ], + ), + ], + ), + ), + const SizedBox( + height: 20, + ), + //TODO each category should return the corresponding products + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(kCategories.length, (index) { + return SizedBox( + width: kCategoriesWidth, + height: kCategoriesHeight, + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 10, + right: 10, + ), + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: NetworkImage(kCategories[index]['imgUrl']), + fit: BoxFit.cover, + ), + borderRadius: BorderRadius.circular(10), + ), + ), + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: Colors.black.withOpacity(0.0), + ), + ), + Positioned( + bottom: 5, + child: Padding( + padding: const EdgeInsets.only(left: 15.0, bottom: 5), + child: Text( + kCategories[index]['title'], + style: const TextStyle( + color: kCategoriesFontColor, + fontSize: kCategoriesFontSize, + fontFamily: kFontFamily2, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ); + }), + ), + ), + const SizedBox( + height: 40, + ), + Padding( + padding: const EdgeInsets.only(left: 15, right: 15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Featured", + style: TextStyle( + fontSize: kCategoriesFontSize, + fontFamily: kFontFamily2, + fontWeight: FontWeight.bold), + ), + Row( + children: [ + TextButton( + onPressed: () { + dev.log("User wants to see all featured products"); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ProductsOverviewScreen("featured")), + ); + }, + child: const Text( + "See all", + style: TextStyle( + color: Colors.grey, + fontSize: kCategoriesFontSize, + fontFamily: kFontFamily2, + ), + ), + ), + const SizedBox( + width: 5, + ), + const Icon( + kForwardSign, + color: Colors.grey, + ) + ], + ), + ], + ), + ), + const SizedBox( + height: 20, + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: List.generate(kFeatured.length, (index) { + return Padding( + padding: const EdgeInsets.only( + left: 15.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: kFeaturedWidth, + height: kFeaturedHeight, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + image: DecorationImage( + image: NetworkImage(kFeatured[index]['imgUrl']), + fit: BoxFit.cover, + ), + ), + ), + const SizedBox( + height: 5, + ), + SizedBox( + width: kFeaturedWidth, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + kFeatured[index]['title'], + style: const TextStyle( + fontFamily: kFontFamily2, + fontWeight: FontWeight.bold, + color: kFeaturedFontColor, + height: 1.5, + fontSize: kProductCardTitleSize), + ), + const SizedBox( + height: 2, + ), + Row( + children: [ + Text( + kFeatured[index]['price'], + style: const TextStyle( + fontFamily: kFontFamily2, + fontWeight: FontWeight.bold, + color: kBottomAppbarIconsAccentColor, + height: 1.5, + fontSize: kProductCardTitleSize - 1, + ), + ), + const Text( + " TL", + style: TextStyle( + fontFamily: kFontFamily2, + fontWeight: FontWeight.bold, + color: kBottomAppbarIconsAccentColor, + height: 1.5, + fontSize: kProductCardTitleSize - 3, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + }), + ), + ), + const SizedBox( + height: 30, + ), + ], + ); + } +} diff --git a/lib/pages/settingspage.dart b/lib/pages/settingspage.dart new file mode 100644 index 0000000..e0163bc --- /dev/null +++ b/lib/pages/settingspage.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../utils/constants.dart'; +import 'dart:developer' as dev; +import 'package:flutter_windowmanager/flutter_windowmanager.dart'; +import 'package:flutter_jailbreak_detection/flutter_jailbreak_detection.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({Key? key}) : super(key: key); + + @override + _SettingsPageState createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State { + bool _secureMode = false; + bool? _jailbroken; + bool? _developerMode; + @override + void initState() { + super.initState(); + initPlatformState(); + } + Future initPlatformState() async { + bool jailbroken; + bool developerMode; + // Platform messages may fail, so we use a try/catch PlatformException. + try { + jailbroken = await FlutterJailbreakDetection.jailbroken; + developerMode = await FlutterJailbreakDetection.developerMode; + } on PlatformException { + jailbroken = true; + developerMode = true; + } + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + setState(() { + _jailbroken = jailbroken; + _developerMode = developerMode; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + body: SingleChildScrollView( + child: Column( + children: [ + ListView.builder( + scrollDirection: Axis.vertical, + shrinkWrap: true, + itemCount: kSettingsPageOptions.length, + itemBuilder: (context, index) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + dev.log("Pressed " + + kSettingsPageOptions[index].toString()); + }, + child: Text( + kSettingsPageOptions[index], + style: const TextStyle( + color: Colors.black, + fontSize: 22, + fontWeight: FontWeight.bold, + fontFamily: kFontFamily2, + ), + ), + ), + Icon( + kForwardSign, + color: Colors.black.withOpacity(0.5), + size: 22, + ), + ], + ); + }, + ), + const SizedBox( + height: 15, + ), + Text('Secure Mode: ${_secureMode.toString()}\n'), + ElevatedButton( + onPressed: () async { + final secureModeToggle = !_secureMode; + + if (secureModeToggle == true) { + await FlutterWindowManager.addFlags( + FlutterWindowManager.FLAG_SECURE); + } else { + await FlutterWindowManager.clearFlags( + FlutterWindowManager.FLAG_SECURE); + } + + setState(() { + _secureMode = !_secureMode; + }); + }, + child: const Text("Toggle Secure Mode"), + ), + const SizedBox( + height: 15, + ), + Text('Jailbroken: ${_jailbroken == null ? "Unknown" : _jailbroken! ? "YES" : "NO"}'), + Text('Developer mode: ${_developerMode == null ? "Unknown" : _developerMode! ? "YES" : "NO"}') + ], + ), + ), + ); + } +} diff --git a/lib/providers/cart_provider.dart b/lib/providers/cart_provider.dart new file mode 100644 index 0000000..8f8eb9b --- /dev/null +++ b/lib/providers/cart_provider.dart @@ -0,0 +1,53 @@ +import 'package:flutter/cupertino.dart'; +import '../helpers/db_helper.dart'; +import 'dart:developer' as dev; + + +class CartItem { + final String id; + final String title; + final String price; + final String imageUrl; + + CartItem({ + required this.id, + required this.title, + required this.price, + required this.imageUrl, + }); +} + +class CartProvider with ChangeNotifier { + List _items = []; + + + List get items { + return [..._items]; + } + + + int getTotalPrice() { + int result = 0; + for (var item in _items) { + result += int.parse(item.price); + } + dev.log("Performing total price calculation: " + result.toString()); + return result; + } + + Future fetchAndSetItems() async { + final dataList = await DBHelper.getData('cart_items'); + + _items = dataList + .map( + (item) => CartItem( + id: item['id'], + title: item['title'], + price: item['price'], + imageUrl: item['imageUrl'], + ), + ) + .toList(); + notifyListeners(); + } +} diff --git a/lib/providers/email_signin_provider.dart b/lib/providers/email_signin_provider.dart new file mode 100644 index 0000000..abbd0ff --- /dev/null +++ b/lib/providers/email_signin_provider.dart @@ -0,0 +1,91 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/cupertino.dart'; +import 'dart:developer' as dev; + +class EmailSignInProvider extends ChangeNotifier { + late bool _isLoading; + late bool _isLogin; + late String _userEmail; + late String _userPassword; + late String _userName; + + + EmailSignInProvider() { + _isLoading = false; + _isLogin = true; + _userEmail = ''; + _userPassword = ''; + _userName = ''; + } + + bool get isLoading => _isLoading; + + set isLoading(bool value) { + _isLoading = value; + notifyListeners(); + } + + bool get isLogin => _isLogin; + + set isLogin(bool value) { + _isLogin = value; + notifyListeners(); + } + + String get userEmail => _userEmail; + + set userEmail(String value) { + _userEmail = value; + notifyListeners(); + } + + String get userPassword => _userPassword; + + set userPassword(String value) { + _userPassword = value; + notifyListeners(); + } + + String get userName => _userName; + + set userName(String value) { + _userName = value; + notifyListeners(); + } + + + + Future login() async { + try { + isLoading = true; + + dev.log(userEmail); + dev.log(userPassword); + + if (isLogin) { + await FirebaseAuth.instance.signInWithEmailAndPassword( + email: userEmail, + password: userPassword, + ); + } else { + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: userEmail, + password: userPassword, + ); + } + + isLoading = false; + return true; + } catch (err) { + dev.log(err.toString()); + isLoading = false; + return false; + } + } + + Future signOut() async { + isLogin = false; + dev.log("Performing firebase sign out"); + return await FirebaseAuth.instance.signOut(); + } +} \ No newline at end of file diff --git a/lib/screens/account_details_screen.dart b/lib/screens/account_details_screen.dart new file mode 100644 index 0000000..36d075d --- /dev/null +++ b/lib/screens/account_details_screen.dart @@ -0,0 +1,162 @@ +import 'package:agu_store_flutter/services/phone_val_api.dart'; +import 'package:agu_store_flutter/utils/constants.dart'; +import 'package:flutter/material.dart'; +import 'dart:developer' as dev; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:overlay_support/overlay_support.dart'; + +class AccountDetailsScreen extends StatefulWidget { + const AccountDetailsScreen({Key? key}) : super(key: key); + + @override + _AccountDetailsScreenState createState() => _AccountDetailsScreenState(); +} + +class _AccountDetailsScreenState extends State { + final firstnameController = TextEditingController(); + final lastnameController = TextEditingController(); + final phoneController = TextEditingController(); + + String? avatarImg; + + @override + void initState() { + + super.initState(); + + firstnameController.addListener(() { + setState(() {}); + }); + lastnameController.addListener(() { + setState(() {}); + }); + phoneController.addListener(() { + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: const Text("Account Details"), + backgroundColor: kMainTheme, + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(15), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Column( + children: [ + const SizedBox( + height: 5, + ), + Card( + margin: const EdgeInsets.all(8), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + children: [ + TextField( + controller: firstnameController, + keyboardType: TextInputType.text, + textInputAction: TextInputAction.done, + decoration: const InputDecoration( + labelText: "Firstname", + border: OutlineInputBorder(), + ), + ), + const SizedBox( + height: 5, + ), + TextField( + controller: lastnameController, + keyboardType: TextInputType.text, + textInputAction: TextInputAction.done, + decoration: const InputDecoration( + labelText: "Lastname", + border: OutlineInputBorder(), + ), + ), + const SizedBox( + height: 5, + ), + TextField( + controller: phoneController, + keyboardType: TextInputType.phone, + decoration: const InputDecoration( + labelText: "Phone", + prefixIcon: Icon(Icons.phone), + hintText: "+901234567890", + border: OutlineInputBorder(), + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 50, + ), + ElevatedButton( + onPressed: () async { + dev.log("Performing user data submit to firestore!"); + dev.log("firstname:" + firstnameController.text); + dev.log("lastname:" + lastnameController.text); + dev.log("phone:" + phoneController.text); + final response = await PhoneValidationApi() + .validatePhone(phoneController.text); + dev.log("phone validation: $response"); + showSimpleNotification( + response == true + ? const Text("Your phone number is validated") + : const Text("Your phone number is not validated"), + background: kMainTheme, + autoDismiss: false, + trailing: Builder(builder: (context) { + return TextButton( + onPressed: () { + OverlaySupportEntry.of(context)?.dismiss(); + }, + child: const Text( + 'Dismiss', + style: TextStyle( + color: Colors.yellow, + ), + ), + ); + }), + ); + //TODO send user data to firestore + }, + style: ElevatedButton.styleFrom( + shape: const StadiumBorder(), + primary: kMainTheme, + fixedSize: const Size( + 340, + 50, + ), + ), + child: const Text( + "Submit", + style: TextStyle( + fontSize: 18, + color: Colors.white, + fontWeight: FontWeight.bold, + fontFamily: kFontFamily2, + ), + ), + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart new file mode 100644 index 0000000..ba4eb7e --- /dev/null +++ b/lib/screens/auth_screen.dart @@ -0,0 +1,181 @@ +import 'package:agu_store_flutter/providers/email_signin_provider.dart'; +import 'package:agu_store_flutter/utils/constants.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +enum AuthMode { signup, login } + +class AuthScreen extends StatefulWidget { + static const routeName = '/auth'; + + const AuthScreen({Key? key}) : super(key: key); + + @override + State createState() => _AuthScreenState(); +} + +class _AuthScreenState extends State { + final _formKey = GlobalKey(); + final _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + final provider = Provider.of(context); + return Scaffold( + key: _scaffoldKey, + // resizeToAvoidBottomInset: false, + body: Stack( + children: [ + Center( + child: Card( + shadowColor: kMainTheme, + margin: const EdgeInsets.all(20), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + buildEmailField(), + if (!provider.isLogin) buildUsernameField(), + buildPasswordField(), + const SizedBox(height: 12), + buildButton(context), + ], + ), + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget buildUsernameField() { + final provider = Provider.of(context); + + return TextFormField( + key: const ValueKey('username'), + autocorrect: true, + textCapitalization: TextCapitalization.words, + enableSuggestions: false, + validator: (value) { + if (value != null && value.isEmpty || + value != null && value.length < 4 || + value != null && value.contains(' ')) { + return 'Please enter at least 4 characters without space'; + } else { + return null; + } + }, + decoration: const InputDecoration(labelText: 'Username'), + onSaved: (username) => provider.userName = username!, + ); + } + + Widget buildButton(BuildContext context) { + final provider = Provider.of(context); + + if (provider.isLoading) { + return const CircularProgressIndicator(); + } else { + return Column( + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: kMainTheme, + fixedSize: const Size(120, 50), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + textStyle: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 18, + fontFamily: kFontFamily2, + ), + ), + child: Text(provider.isLogin ? 'Login' : 'Signup'), + onPressed: () => submit(), + ), + TextButton( + child: Text( + provider.isLogin + ? 'Create new account' + : 'I already have an account', + style: const TextStyle( + color: kMainTheme, + fontSize: 16, + ), + ), + onPressed: () => provider.isLogin = !provider.isLogin, + ), + ], + ); + } + } + + Widget buildEmailField() { + final provider = Provider.of(context); + + return TextFormField( + key: const ValueKey('email'), + autocorrect: false, + textCapitalization: TextCapitalization.none, + enableSuggestions: false, + validator: (value) { + const pattern = r'(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)'; + final regExp = RegExp(pattern); + + if (!regExp.hasMatch(value!)) { + return 'Enter a valid mail'; + } else { + return null; + } + }, + keyboardType: TextInputType.emailAddress, + decoration: const InputDecoration(labelText: 'Email address'), + onSaved: (email) => provider.userEmail = email!, + ); + } + + Widget buildPasswordField() { + final provider = Provider.of(context); + + return TextFormField( + key: const ValueKey('password'), + validator: (value) { + if (value != null && value.isEmpty || + value != null && value.length < 7) { + return 'Password must be at least 7 characters long.'; + } else { + return null; + } + }, + decoration: const InputDecoration(labelText: 'Password'), + obscureText: true, + onSaved: (password) => provider.userPassword = password!, + ); + } + + Future submit() async { + final provider = Provider.of(context, listen: false); + + final isValid = _formKey.currentState?.validate(); + FocusScope.of(context).unfocus(); + + if (isValid!) { + _formKey.currentState?.save(); + + final isSuccess = await provider.login(); + + if (isSuccess) { + //attach orders to user + } + } + } +} diff --git a/lib/screens/category_overview.dart b/lib/screens/category_overview.dart new file mode 100644 index 0000000..0c32c75 --- /dev/null +++ b/lib/screens/category_overview.dart @@ -0,0 +1,77 @@ +import 'package:agu_store_flutter/utils/constants.dart'; +import 'package:agu_store_flutter/widgets/category_ui.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import '../main.dart'; +import '../services/product_db_connector.dart'; +import 'package:flutter/material.dart'; + +// This will return the products with given category id +// Because there are multiple categories +class CategoryOverviewScreen extends StatelessWidget { + CategoryOverviewScreen({Key? key}) : super(key: key); + final dbCategories = ProductDBConnector(); + + @override + Widget build(BuildContext context) { + //24 is for notification bar on Android + var size = MediaQuery.of(context).size; + final double itemHeight = (size.height - 160 - kToolbarHeight) / 2; + final double itemWidth = size.width / 2; + return Scaffold( + appBar: AppBar( + leading: Builder( + builder: (BuildContext context) { + return IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const RootPage()), + ); + }, + tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip, + ); + }, + ), + title: const Text("Categories"), + backgroundColor: kMainTheme, + centerTitle: true, + titleTextStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + fontFamily: kFontFamily2, + ), + ), + body: StreamBuilder( + stream: dbCategories.getCategories(), + builder: ( + BuildContext ctx, + AsyncSnapshot snapshot, + ) { + if (snapshot.hasError) { + return const Text("Something went wrong :/"); + } + if (snapshot.connectionState == ConnectionState.waiting) { + return const Text("Loading..."); + } + final categoriesDocRef = snapshot.requireData; + return GridView.builder( + padding: const EdgeInsets.all(10.0), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: (itemWidth / itemHeight), + crossAxisSpacing: 10, + mainAxisSpacing: 10, + ), + itemCount: categoriesDocRef.size, + itemBuilder: (ctx, i) => CategoryCard( + categoriesDocRef.docs[i]['id'], + categoriesDocRef.docs[i]['label'], + categoriesDocRef.docs[i]['img'], + ), + ); + }, + ), + ); + } +} diff --git a/lib/screens/product_details_screen.dart b/lib/screens/product_details_screen.dart new file mode 100644 index 0000000..bd11eb0 --- /dev/null +++ b/lib/screens/product_details_screen.dart @@ -0,0 +1,137 @@ +import 'package:agu_store_flutter/providers/cart_provider.dart'; +import 'package:agu_store_flutter/utils/constants.dart'; +import 'package:flutter/material.dart'; +import 'dart:developer' as dev; +import '../helpers/db_helper.dart'; +import 'package:provider/provider.dart'; +import 'package:share_plus/share_plus.dart'; + +class ProductDetailsScreen extends StatelessWidget { + final String id; + final String title; + final String description; + final String price; + final String imageUrl; + + //TODO add thumbnail images for the product for scrollable images + + ProductDetailsScreen( + this.id, + this.title, + this.description, + this.price, + this.imageUrl, + ); + + TextEditingController textdate = TextEditingController(); + + @override + Widget build(BuildContext context) { + final cart = Provider.of(context, listen: false); + return Scaffold( + appBar: AppBar( + backgroundColor: kMainTheme, + centerTitle: true, + title: Text( + title, + style: const TextStyle( + color: Colors.white, + fontFamily: kFontFamily2, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + actions: [ + IconButton( + icon: const Icon( + Icons.share, + ), + onPressed: () { + Share.share('Product Cost is $price\n, $imageUrl'); + }, + ) + ], + //actions [IconButton(onPressed: (){},icon: Icon(Icons.share),),] + ), + body: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + ), + child: Container( + height: 250, + width: MediaQuery.of(context).size.width, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + image: DecorationImage( + image: NetworkImage(imageUrl), + fit: BoxFit.cover, + ), + ), + ), + ), + Column( + children: [ + const Text( + "Product Details", + style: TextStyle( + fontSize: 32, + color: Colors.black, + fontFamily: kFontFamily2, + ), + ), + Text( + description, + style: const TextStyle( + fontSize: 32, + color: Colors.black, + fontFamily: kFontFamily2, + ), + ), + Text( + "Price: $price TL", + style: const TextStyle( + fontSize: 28, + color: Colors.black, + fontFamily: kFontFamily2, + ), + ), + ], + ), + ElevatedButton( + onPressed: () { + dev.log("Request to add {item #$id - $title} to Cart"); + //cart.addItem(id, price, title, imageUrl); + DBHelper.insert('cart_items', { + 'id': id, + 'title': title, + 'price': price, + 'imageUrl': imageUrl + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('$title Added Your Card'))); + }, + style: ElevatedButton.styleFrom( + primary: kMainTheme, + fixedSize: const Size(340, 50), + shape: const StadiumBorder(), + ), + child: const Text( + "Add to Cart", + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + fontFamily: kFontFamily2, + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/screens/products_overview.dart b/lib/screens/products_overview.dart new file mode 100644 index 0000000..32b1014 --- /dev/null +++ b/lib/screens/products_overview.dart @@ -0,0 +1,64 @@ +import 'package:agu_store_flutter/utils/constants.dart'; +import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import '../widgets/product_ui.dart'; +import '../services/product_db_connector.dart'; + +class ProductsOverviewScreen extends StatelessWidget { + String categoryTitle; + ProductsOverviewScreen(this.categoryTitle, {Key? key}) : super(key: key); + + final dbCategories = ProductDBConnector(); + + @override + Widget build(BuildContext context) { + //24 is for notification bar on Android + var screenSize = MediaQuery.of(context).size; + final double itemHeight = (screenSize.height - kToolbarHeight) / 2; + final double itemWidth = screenSize.width / 2; + return Scaffold( + appBar: AppBar( + title: Text(categoryTitle), + backgroundColor: kMainTheme, + centerTitle: true, + titleTextStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 22, + fontFamily: kFontFamily2, + ), + ), + body: StreamBuilder( + stream: dbCategories.getCategoryProducts(categoryTitle), + builder: ( + BuildContext ctx, + AsyncSnapshot snapshot, + ) { + if (snapshot.hasError) { + return const Text("Something went wrong :/"); + } + if (snapshot.connectionState == ConnectionState.waiting) { + return const Text("Loading..."); + } + final productsDocRef = snapshot.requireData; + return GridView.builder( + padding: const EdgeInsets.all(10.0), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: (itemWidth / itemHeight), + crossAxisSpacing: 10, + mainAxisSpacing: 10, + ), + itemCount: productsDocRef.size, + itemBuilder: (ctx, i) => ProductCard( + productsDocRef.docs[i]['id'], + productsDocRef.docs[i]['title'], + productsDocRef.docs[i]['price'], + productsDocRef.docs[i]['description'], + productsDocRef.docs[i]['img'], + ), + ); + }, + ), + ); + } +} diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart new file mode 100644 index 0000000..47d7901 --- /dev/null +++ b/lib/screens/profile_screen.dart @@ -0,0 +1,120 @@ +import 'package:agu_store_flutter/providers/email_signin_provider.dart'; +import 'package:agu_store_flutter/utils/constants.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'dart:developer' as dev; +import 'package:provider/provider.dart'; + +import 'account_details_screen.dart'; + +class ProfileScreen extends StatelessWidget { + const ProfileScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final user = FirebaseAuth.instance.currentUser; + final userEmail = user?.email; + return Scaffold( + body: ListView( + children: [ + Padding( + padding: const EdgeInsets.only(left: 10.0, right: 10.0, top: 2), + child: Row( + children: [ + const SizedBox( + width: 20, + ), + Flexible( + fit: FlexFit.tight, + child: Column( + children: [ + Text( + userEmail! + " Logged in", + maxLines: 3, + style: const TextStyle( + fontSize: 25, + fontWeight: FontWeight.bold, + fontFamily: kFontFamily2, + ), + ), + ], + ), + ), + const SizedBox( + height: 100, + ), + ], + ), + ), + const Divider( + thickness: 1, + ), + Padding( + padding: const EdgeInsets.only( + left: 15.0, + right: 30.0, + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 15.0, + ), + child: ListView.builder( + scrollDirection: Axis.vertical, + shrinkWrap: true, + itemCount: kAccountPageOptions.length, + itemBuilder: (context, index) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () { + // Account Details button + if (index == 0) { + dev.log("Pressed " + + kAccountPageOptions[index].toString()); + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const AccountDetailsScreen()), + ); + } + + // Log out Button + if (index == kAccountPageOptions.length - 1) { + final provider = + Provider.of(context, + listen: false); + provider.signOut(); + dev.log("Pressed " + + kAccountPageOptions[index].toString()); + } + }, + child: Text( + kAccountPageOptions[index], + style: const TextStyle( + color: Colors.black, + fontSize: 22, + fontWeight: FontWeight.bold, + fontFamily: kFontFamily2, + ), + ), + ), + Icon( + kForwardSign, + color: Colors.black.withOpacity(0.5), + size: 22, + ), + ], + ); + }, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/services/phone_val_api.dart b/lib/services/phone_val_api.dart new file mode 100644 index 0000000..0b5a432 --- /dev/null +++ b/lib/services/phone_val_api.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:http/http.dart' as http; + +class PhoneValidationApi +{ + static final _apiKey = dotenv.env['PHONE_API']!; + static const _baseUrl = 'https://phonevalidation.abstractapi.com/v1/'; + + bool? isValidPhoneNumber; + + Future validatePhone(String phone) async + { + final request = Uri.parse(_baseUrl+ "?api_key=" + _apiKey + "&phone=" + phone); + final response = await http.get(request); + print(json.decode(response.body)); + Map? data = jsonDecode(response.body); + return data!['valid']; + } +} \ No newline at end of file diff --git a/lib/services/product_db_connector.dart b/lib/services/product_db_connector.dart new file mode 100644 index 0000000..561f351 --- /dev/null +++ b/lib/services/product_db_connector.dart @@ -0,0 +1,31 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'dart:developer' as dev; + +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; + +class ProductDBConnector +{ + static const _collectionLabel = 'categories'; + // Get an individual category's products + Stream? getCategoryProducts(String categoryTitle) + { + if(categoryTitle.isEmpty) { + return null; + } + categoryTitle = categoryTitle.toLowerCase(); + Stream products = FirebaseFirestore.instance.collection("/categories/$categoryTitle/products").snapshots(); + dev.log("$categoryTitle products data has been retrieved successfully!"); + return products; + } + + // Get categories from the database + Stream? getCategories() + { + Stream categories = FirebaseFirestore.instance.collection("categories").snapshots(); + dev.log("Category data has been retrieved successfully!"); + return categories; + } + + + +} \ No newline at end of file diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart new file mode 100644 index 0000000..d8c06d1 --- /dev/null +++ b/lib/utils/constants.dart @@ -0,0 +1,194 @@ +import 'package:flutter/material.dart'; + +// APP +const String kAppTitle = 'AGU STORE'; +const String kFontFamily1 = 'Open Sans'; +const String kFontFamily2 = 'Exo 2'; +const Color kMainTheme = Color(0xffff0000); +const int kMainThemeColorHex = 0xffff1d1d; + +// HOMEPAGE +const double kHomeImgHeight = 500; +const double kHomePageImgTitleSize = 22.0; +const Color kHomePageImgTitleColor = Color(0xffffffff); +const IconData kForwardSign = Icons.arrow_forward_ios_outlined; +// This category is supposed to be customizable +const String kHomePageImgCategory = "AGU Collection"; + +// HOMEPAGE -> CATEGORIES +const double kCategoriesFontSize = 20; +const Color kCategoriesFontColor = Color(0xffffffff); +const double kCategoriesWidth = 180; +const double kCategoriesHeight = 220; + +// HOMEPAGE -> FEATURED as ProductCard +const Color kFeaturedFontColor = Color(0xff000000); +const double kFeaturedWidth = 180; +const double kFeaturedHeight = 240; +const double kProductCardTitleSize = 16; + +// CARTPAGE +const Color kCartPageBackground = Color(0xffffffff); +const Color kCartPagePromotionColor = Color(kMainThemeColorHex); +const double kCartPageWidth = 140; +const double kCartPageHeight = 180; +const IconData kRemoveSign = Icons.remove_outlined; +const IconData kAddSign = Icons.add_outlined; +// ACCOUNT PAGE +const kHintTextStyle = TextStyle( + color: Colors.white54, + fontFamily: 'OpenSans', +); + +const kLabelStyle = TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontFamily: kFontFamily1, +); + +final kBoxDecorationStyle = BoxDecoration( + color: const Color(0xFFFFFFFf), + borderRadius: BorderRadius.circular(10.0), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 6.0, + offset: Offset(0, 2), + ), + ], +); +const kFacebookLogo = "https://github.com/MarcusNg/flutter_login_ui/blob/master/assets/logos/facebook.jpg?raw=true"; +const kGoogleLogo = "https://github.com/MarcusNg/flutter_login_ui/blob/master/assets/logos/google.jpg?raw=true"; +// ROOT APP +const Color kRootAppBackground = Color(0xffffffff); + +// TOP BAR +const Color kTopAppBarBackgroundColor = Color(0xffffffff); +const Color kTopAppBarIconsColor = Color(0xffffffff); +const IconData kSearchButtonIcon = Icons.search_outlined; +const IconData kAuthButton = Icons.login_outlined; +const double kTopAppBarIconsSize = 35; + +// BOTTOM BAR +const double kBottomBarIconsSize = 35; +const Color kBottomAppbarBackgroundColor = Color(0xffffffff); +const Color kBottomAppbarIconsColor = Color(0xff000000); +const Color kBottomAppbarIconsAccentColor = Color(kMainThemeColorHex); +const IconData kHomePageIcon = Icons.home_outlined; +const IconData kShoppingCartIcon = Icons.shopping_cart_outlined; +const IconData kAccountIcon = Icons.account_circle_outlined; +const IconData kSettingsIcon = Icons.settings_outlined; + +// CATEGORIES +const List> kCategories = [ + { + 'title': 'Men', + 'imgUrl': 'https://images.unsplash.com/photo-1529809773508-cd894c3de760?ixid=MnwxMjA3fDB8MHxzZWFyY2h8Mnx8bWVuJTIwY2xvdGhpbmd8ZW58MHx8MHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60', + }, + { + 'title': 'Women', + 'imgUrl': 'https://media.istockphoto.com/photos/the-perfect-dress-for-me-picture-id660490044?b=1&k=20&m=660490044&s=170667a&w=0&h=JadgZcpyNQJymDjgztzC5CCg2UsCGoS9XYEv04TSHNw=', + }, + { + 'title': 'Souvenir', + 'imgUrl': 'https://images.unsplash.com/photo-1593607563703-358c52f28688?ixid=MnwxMjA3fDB8MHxzZWFyY2h8M3x8c291dmVuaXJ8ZW58MHx8MHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=500&q=60', + }, + { + 'title': 'Book', + 'imgUrl': 'https://images.unsplash.com/photo-1548092176-dff0757b8ee6?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MjMyfHxib29rfGVufDB8fDB8fA%3D%3D&auto=format&fit=crop&w=500&q=60', + }, + { + 'title': 'Furniture', + 'imgUrl': 'https://images.unsplash.com/photo-1618220179428-22790b461013?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MTZ8fGZ1cm5pdHVyZXxlbnwwfHwwfHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60', + }, +]; + +// HOMEPAGE +//TODO select kHomeImg randomly from a database +const String kHomeImg = 'https://images.unsplash.com/flagged/photo-1561338484-01bbb4026c7a?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=687&q=80'; +const String kSplashScreen = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAL0AAAELCAMAAAC77XfeAAAAwFBMVEX////SNzvOFhz///0rKyvVQEPSNTnRLDHtvL7PISb67e7PJSzRLzTffHzPJSrWQUfjkJLZX2HxzM0AAADnl5obGxv32NkdHR329vYPDw8lJSWZmZn75eaoqKiIiIjT09MLCwsvLy9XV1e5ubmQkJDV1dXCwsJ+fn7IyMjj4+OEhISioqJiYmLh4eGwsLDMAABra2tLS0s4ODg9PT3ZS1LVU1fhiYzbZGrgfoDyyMnNBRHrrq/25eXsuLloaGhzc3Otb7wRAAAFbElEQVR4nO2bbVviOBSG6xBRlGUcuzVNXKBQCuWlrOA4O7o78v//1Z60BRpApDq00eu5P4x4eix305OXhsGyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPhdVA7iLckFuBf2TsegYl1VTw/ge5J+Xj8kuX5RmP/VZfUAvqb2tUOSawXan528zsq+cUDyCexhD3vYw95we23Oqdf326uMDNWy7avfdE6re+yrD3ry92rJ9meufuhbfY997UZP/vOsZPtL7V0rr9hf6+eBfX5gr4B9fmCvgH1+YK+AfX5+PNWWPL3H/mp9nr+Ls7/4a412IKf9i+cpjIr+Sy577U+L2mDUNk/fYV/Jnqog9328o+0N4BPZnyaxjH3j2ujt84r1z8nmDvhj/euSutlt/2rTmtz0AAAzMWJWfzMVWiF/+d0UuEI+aAc8H6bt38Me9rCHPew/lX39HZT+iWf9j7fz88G0TzxzYdgubE5gnx/YK4yzz2yrbv9v2M3zGGaf86HRNHvr+nFFuu13sY48bigaZ/9Qa6TUqkns/GkZaTw96ucxzv4DfOoGewXs8wN7BezzA3sF7PMDewXs8wN7Bezz80ntX/v2gGH21bPHG42Hfd/cuPxXT/5ZL9f+5KRRy9JY++z63smZllzLfMfGtP37D/GNJdjDHvawh/3LVHLaXxplb1k/vly+SqOx3kM+IPupuLa/+XV+CEn6fwcl/3rPJ6cAAAAAAMfh7oV4e9y6m80789ldqxnSMiZop4Tq6DCI/w2XsZLWOb4d7oiGC+E5XErJmJTcsceWNV3YHmE/02urZduS/IMeU0G70xsW7Z0w492tWH/mkbOw+bwjbM+RzGmqcCgY8/w4waar6sS5XcnkrEjhLIHHvEgPuRNPMsF67bga3GA8E4k9maaiPYcxZsfHA8HErrtXCHeS8YEWiSRnnPnZUN9O7Huc9+IXPrU94/FLl6eXUQJDpSGy7x4IavjFhk9rHP+4dVJ769kWaX9xpf73RdLjZO/crgMu1bZobab5Scba3mr7aT8t095j8n5ZAzHUCfn2GBol7ZyxX1OefdORiz51u1WV+x7dixfTd9qz0uzvpde2OLX/OpC5lC3Msg89NQLeOoyuIQ0w1nk53yz7rlQNTR1VPieBAQ2W234rdtp3SrKP7KTGydlLRpA52zv1GGU/4E48jkce4/Eg6dLleHtWLCbZk2s6S07SUY8ugzl7VEyyJ5dR8oo6a3wXdtiHzWlCc2qWvWT2skrmMu4Bat0g9MoJ+n7Hc4TdDEOj7H0hV5Oq7zDRp5/2rl7rOmm3MMl+JuWklTKiRbpa5qt1wtYih6LpCtkcezUxcWcJVY4XqJXDrtkqa799baXYT1RzKyaKGf22SFeY083UjL0cbB6k9X3x9jRTaU8ltMBRI716aNoaMzP223dGPVsVbk/zq50dXahmVE1X5mkPyLKy90VcX5snmh9TdBc0U8mJFlCNbiWDprPQk1f2dHDz0qjp+VY1HRt6pvL0oZHaMK74ttLvarWwsreeOXNG2UND6u725u04Nm52TZ8Q0GNWXAIBp+Jxsl13be/SMdFdVZw7VgujPYvS40Ddbym0Qg2a8e1wuzSYCjby20EUBe3+9H6dHF+aNxn7Ydi/feZUbnbhdROpgXFH91t2hakUajdKqL0zIUhRrC7V7drq1ghCTRLO9vh6ZIZN2+HcnkaZXahhNBwICraCpCz8LvdEMpHRRTjZQmo/C6H2CCV3vM5t4YPltNWLaWUK1h+lwdGyL7tRfzommn4YbSi6/d6CZrhBs+juCgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAD8b/+xPCc/Snex8AAAAASUVORK5CYII='; + +// BOTTOM BAR ITEMS +const List kItemsTab = [ + {"icon": kHomePageIcon, "size": kBottomBarIconsSize}, + {"icon": kShoppingCartIcon, "size": kBottomBarIconsSize}, + {"icon": kAccountIcon, "size": kBottomBarIconsSize}, + {"icon": kSettingsIcon, "size": kBottomBarIconsSize}, +]; + + +// FEATURED PRODUCTS +const List> kFeatured = [ + { + 'title': 'Summer Loose Korean T-shirt', + 'price': '30', + 'imgUrl': 'https://images.unsplash.com/photo-1581044777550-4cfa60707c03?ixid=MXwxMjA3fDB8MHxzZWFyY2h8MTF8fGZhc2hpb258ZW58MHx8MHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', + }, + { + 'title': 'Bat Sleeve Student T-shirt Summer', + 'price': '35', + 'imgUrl': 'https://images.unsplash.com/photo-1545291730-faff8ca1d4b0?ixid=MXwxMjA3fDB8MHxzZWFyY2h8Mjd8fGZhc2hpb258ZW58MHx8MHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=60', + }, + { + 'title': 'Summer New Korean Version', + 'price': '25', + 'imgUrl': 'https://images.unsplash.com/photo-1562572159-4efc207f5aff?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=400&q=60', + }, + { + 'title': 'Loose-fitting Outside Shirt', + 'price': '30', + 'imgUrl': 'https://images.unsplash.com/photo-1503185912284-5271ff81b9a8?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60', + }, + { + 'title': 'Cotton Short-sleeved T-shirt', + 'price': '20', + 'imgUrl': 'https://images.unsplash.com/photo-1541257710737-06d667133a53?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60', + } +]; + +// PRODUCTS IN THE CART +const List kCart = [ + { + "img": + "https://images.unsplash.com/photo-1495385794356-15371f348c31?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=60", + "name": "Snoopy T-shirt", + "ref": "04559812", + "price": "40 TL", + "size": "S" + }, + { + "img": + "https://images.unsplash.com/photo-1545291730-faff8ca1d4b0?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=800&q=60", + "name": "American", + "ref": "04459811", + "price": "30 TL", + "size": "M" + }, +]; + +// USER PROFILE +const String kUserProfile = 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1170&q=80'; + +// ACCOUNT PAGE OPTIONS +const List kAccountPageOptions = [ + 'Account Details', + 'Addresses', + 'Orders', + 'My Lists', + 'Credit/Debit Cards', + 'AGU Store Wallet', + 'Delivery Information', + 'Payment Information', + 'Log out', +]; + +// SETTINGS PAGE OPTIONS +const List kSettingsPageOptions = [ + 'Notification Settings', + 'Language', + 'Give Us Feedback', + 'Privacy Settings', + 'About Us', +]; +const String kSettingsMsg = "https://images.unsplash.com/photo-1495106245177-55dc6f43e83f?ixid=MnwxMjA3fDB8MHxzZWFyY2h8MzF8fGhhcHB5JTIwYmFubmVyfGVufDB8fDB8fA%3D%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60"; +const String kAccountMsg = "https://images.pexels.com/photos/7564223/pexels-photo-7564223.jpeg?auto=compress&cs=tinysrgb&h=750&w=1260"; diff --git a/lib/utils/http_exception.dart b/lib/utils/http_exception.dart new file mode 100644 index 0000000..d11795d --- /dev/null +++ b/lib/utils/http_exception.dart @@ -0,0 +1,10 @@ +class HttpException implements Exception { + final String message; + + HttpException(this.message); + + @override + String toString() { + return message; + } +} diff --git a/lib/widgets/category_ui.dart b/lib/widgets/category_ui.dart new file mode 100644 index 0000000..31bea6a --- /dev/null +++ b/lib/widgets/category_ui.dart @@ -0,0 +1,60 @@ +import 'package:agu_store_flutter/utils/constants.dart'; +import 'package:flutter/material.dart'; +import '../screens/products_overview.dart'; +import 'dart:developer' as dev; + +//TODO Parse category title -> remove '_' title[0] is uppercase +class CategoryCard extends StatelessWidget { + final String id; + final String title; + final String img; + + CategoryCard( + this.id, + this.title, + this.img, + ); + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(10), + child: GridTile( + child: Container( + height: kCategoriesHeight, + width: kCategoriesWidth, + child: GestureDetector( + onTap: () { + dev.log("Request to get category #$title data"); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ProductsOverviewScreen(title)), + ); + }, + ), + decoration: BoxDecoration( + image: DecorationImage( + image: NetworkImage( + img, + ), + fit: BoxFit.fill, + ), + ), + ), + footer: GridTileBar( + backgroundColor: Colors.black54, + title: Text( + title, + style: const TextStyle( + fontFamily: kFontFamily2, + fontSize: 18, + fontWeight: FontWeight.w400, + ), + textAlign: TextAlign.center, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/product_ui.dart b/lib/widgets/product_ui.dart new file mode 100644 index 0000000..bce7667 --- /dev/null +++ b/lib/widgets/product_ui.dart @@ -0,0 +1,112 @@ +import 'package:agu_store_flutter/screens/product_details_screen.dart'; +import 'package:agu_store_flutter/utils/constants.dart'; +import 'package:flutter/material.dart'; +import 'dart:developer' as dev; + +class ProductCard extends StatelessWidget { + final String id; + final String title; + final String price; + final String description; + final String imageUrl; + + ProductCard( + this.id, + this.title, + this.price, + this.description, + this.imageUrl, + ); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => ProductDetailsScreen(id, title, description, price, imageUrl)), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 3, + child: SizedBox( + height: kFeaturedHeight, + child: Container( + width: kFeaturedWidth, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + image: DecorationImage( + image: NetworkImage(imageUrl), + fit: BoxFit.cover, + ), + ), + ), + ), + ), + Flexible( + flex: 1, + child: SizedBox( + width: kFeaturedWidth, + height: kFeaturedHeight, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + flex: 2, + child: Text( + title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontFamily: kFontFamily2, + fontWeight: FontWeight.bold, + color: kFeaturedFontColor, + height: 1.5, + fontSize: kProductCardTitleSize), + ), + ), + Flexible( + flex: 1, + child: Row( + children: [ + Flexible( + flex: 2, + child: Text( + price.toString(), + style: const TextStyle( + fontFamily: kFontFamily2, + fontWeight: FontWeight.bold, + color: kBottomAppbarIconsAccentColor, + height: 1.5, + fontSize: kProductCardTitleSize - 1, + ), + ), + ), + const Flexible( + flex: 2, + child: Text( + " TL", + style: TextStyle( + fontFamily: kFontFamily2, + fontWeight: FontWeight.bold, + color: kBottomAppbarIconsAccentColor, + height: 1.5, + fontSize: kProductCardTitleSize - 3, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..5176f03 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,140 @@ +name: agu_store_flutter +description: A new Flutter project. + +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +version: 1.0.0+1 + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + flutter_native_splash: ^1.3.3 + cupertino_icons: ^1.0.4 + # System and Broadcast Receivers + connectivity_plus: ^2.2.0 + share_plus: ^3.0.4 + # Security + flutter_windowmanager: ^0.2.0 + flutter_dotenv: ^5.0.2 + flutter_jailbreak_detection: ^1.8.0 + # State management + provider: ^6.0.2 + firebase_core: ^1.10.6 + # Google sign-in + firebase_auth: ^3.3.4 + google_sign_in: ^5.2.1 + font_awesome_flutter: ^9.2.0 + cloud_firestore: ^3.1.5 + http: ^0.13.4 + overlay_support: ^1.2.1 + flutter_local_notifications: ^9.2.0 + firebase_messaging: ^11.2.5 + sqflite: + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 + flutter_launcher_icons: ^0.9.2 + + +flutter_icons: + android: "launcher_icon" + ios: true + image_path: "assets/images/icon.png" + +flutter: + + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - .env + # - images/a_dot_ham.jpeg + fonts: + - family: Exo 2 + fonts: + - asset: assets/fonts/Exo2-VariableFont_wght.ttf + - asset: assets/fonts/Exo2-Italic-VariableFont_wght.ttf + style: italic + - family: Open Sans + fonts: + - asset: assets/fonts/OpenSans-VariableFont_wdth,wght.ttf + - asset: assets/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf + style: italic + +# USAGE: https://pub.dev/packages/flutter_native_splash +flutter_native_splash: + + # This package generates native code to customize Flutter's default white native splash screen + # with background color and splash image. + # Customize the parameters below, and run the following command in the terminal: + # flutter pub run flutter_native_splash:create + # To restore Flutter's default white splash screen, run the following command in the terminal: + # flutter pub run flutter_native_splash:remove + + # color or background_image is the only required parameter. Use color to set the background + # of your splash screen to a solid color. Use background_image to set the background of your + # splash screen to a png image. This is useful for gradients. The image will be stretch to the + # size of the app. Only one parameter can be used, color and background_image cannot both be set. + #color: "#FF002B" + background_image: "assets/images/splash_screen.png" + + # Optional parameters are listed below. To enable a parameter, uncomment the line by removing + # the leading # character. + + # The image parameter allows you to specify an image used in the splash screen. It must be a + # png file and should be sized for 4x pixel density. + #image: assets/images/splash_screen.png + + # The color_dark, background_image_dark, and image_dark are parameters that set the background + # and image when the device is in dark mode. If they are not specified, the app will use the + # parameters from above. If the image_dark parameter is specified, color_dark or + # background_image_dark must be specified. color_dark and background_image_dark cannot both be + # set. + #color_dark: "#042a49" + #background_image_dark: "assets/dark-background.png" + #image_dark: assets/splash-invert.png + + # The android, ios and web parameters can be used to disable generating a splash screen on a given + # platform. + #android: false + #ios: false + #web: false + + # The position of the splash image can be set with android_gravity, ios_content_mode, and + # web_image_mode parameters. All default to center. + # + # android_gravity can be one of the following Android Gravity (see + # https://developer.android.com/reference/android/view/Gravity): bottom, center, + # center_horizontal, center_vertical, clip_horizontal, clip_vertical, end, fill, fill_horizontal, + # fill_vertical, left, right, start, or top. + #android_gravity: center + # + # ios_content_mode can be one of the following iOS UIView.ContentMode (see + # https://developer.apple.com/documentation/uikit/uiview/contentmode): scaleToFill, + # scaleAspectFit, scaleAspectFill, center, top, bottom, left, right, topLeft, topRight, + # bottomLeft, or bottomRight. + #ios_content_mode: center + # + # web_image_mode can be one of the following modes: center, contain, stretch, and cover. + #web_image_mode: center + + # To hide the notification bar, use the fullscreen parameter. Has no affect in web since web + # has no notification bar. Defaults to false. + # NOTE: Unlike Android, iOS will not automatically show the notification bar when the app loads. + # To show the notification bar, add the following code to your Flutter app: + # WidgetsFlutterBinding.ensureInitialized(); + # SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom, SystemUiOverlay.top]); + #fullscreen: true + + # If you have changed the name(s) of your info.plist file(s), you can specify the filename(s) + # with the info_plist_files parameter. Remove only the # characters in the three lines below, + # do not remove any spaces: + #info_plist_files: + # - 'ios/Runner/Info-Debug.plist' + # - 'ios/Runner/Info-Release.plist' \ No newline at end of file