diff --git a/.metadata b/.metadata index 784ce129..90eabcff 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "a14f74ff3a1cbd521163c5f03d68113d50af93d3" + revision: "80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819" channel: "stable" project_type: app @@ -13,11 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 + base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 + - platform: android + create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 + base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 + - platform: ios + create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 + base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 + - platform: linux + create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 + base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 + - platform: macos + create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 + base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 - platform: web - create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 - base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 + base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 + - platform: windows + create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 + base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819 # User provided section diff --git a/README.md b/README.md index 451d649a..541bb3c5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # get-flutter-fire +CONTRIBUTOR: Vaishvi Jigneshkumar Parikh +COLLEGE NAME: MIT World Peace University +CONTACT DETAILS: 1032211203@mitwpu.edu.in + This codebase provides a boilerplate code utilizing the following three technologies: 1. Flutter 3.0 - For UX and uses Dart languange. See [https://flutter.dev/] diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 00000000..dc9b9ab3 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "projectnumber", + "project_id": "sharekhanproject", + "storage_bucket": "sharekhanproject.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "app_id", + "android_client_info": { + "package_name": "com.example.get_flutter_fire" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "MYKEY" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/get_flutter_fire/MainActivity.kt b/android/app/src/main/kotlin/com/example/get_flutter_fire/MainActivity.kt new file mode 100644 index 00000000..018e286b --- /dev/null +++ b/android/app/src/main/kotlin/com/example/get_flutter_fire/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.get_flutter_fire + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/assets/lottie/tick.json b/assets/lottie/tick.json new file mode 100644 index 00000000..2ba17b35 --- /dev/null +++ b/assets/lottie/tick.json @@ -0,0 +1 @@ +{"v":"5.4.2","fr":25,"ip":0,"op":50,"w":1080,"h":1080,"nm":"Tick","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[-214,-44,0],"ix":1},"s":{"a":0,"k":[71.036,71.036,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-392,-22],[-288,82],[-36,-170]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":60,"ix":5},"lc":2,"lj":2,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[0.999]},"o":{"x":[0.112],"y":[0]},"n":["0_0p999_0p112_0"],"t":17,"s":[0],"e":[100]},{"t":35}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":50,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[46,70,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"n":["0_1_0p333_0","0_1_0p333_0","0p667_1_0p333_0"],"t":9,"s":[0,0,100],"e":[84.593,84.593,100]},{"t":17}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[540,540],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.598895203833,0.149973072725,0.83137254902,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[46,70],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":50,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[540,540,0],"ix":2},"a":{"a":0,"k":[46,70,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.146,0.146,0.667],"y":[1,1,1]},"o":{"x":[0.21,0.21,0.333],"y":[0,0,0]},"n":["0p146_1_0p21_0","0p146_1_0p21_0","0p667_1_0p333_0"],"t":0,"s":[0,0,100],"e":[100,100,100]},{"t":8}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[540,540],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.830080817727,0.501960754395,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[46,70],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":50,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/cors.json b/cors.json new file mode 100644 index 00000000..a4859301 --- /dev/null +++ b/cors.json @@ -0,0 +1,14 @@ +[ + { + "origin": [ + "*" + ], + "method": [ + "GET", + "POST", + "PUT", + "DELETE" + ], + "maxAgeSeconds": 3600 + } +] \ No newline at end of file diff --git a/firebase.json b/firebase.json new file mode 100644 index 00000000..d6ea5ad1 --- /dev/null +++ b/firebase.json @@ -0,0 +1 @@ +// Added my firebase.json file \ No newline at end of file diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 00000000..a940fe30 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + API-KEY + GCM_SENDER_ID + SENDER-ID + PLIST_VERSION + 1 + BUNDLE_ID + com.example.getflutterfire + PROJECT_ID + sharekhanproject + STORAGE_BUCKET + sharekhanproject.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + GOOGLE-APP-ID + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index fc7f1dd4..41297579 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -51,5 +51,12 @@ Allow access to microphone for video recording NSPhotoLibraryUsageDescription Allow access to photo library + NSPhotoLibraryUsageDescription + Need access to photo library to select profile picture + NSCameraUsageDescription + Need access to camera to take a profile picture + NSMicrophoneUsageDescription + Need access to microphone for recording + diff --git a/lib/app/modules/cart/controllers/cart_controller.dart b/lib/app/modules/cart/controllers/cart_controller.dart index c938ec4c..71344eb7 100644 --- a/lib/app/modules/cart/controllers/cart_controller.dart +++ b/lib/app/modules/cart/controllers/cart_controller.dart @@ -1,9 +1,11 @@ import 'package:get/get.dart'; +import 'package:get_flutter_fire/models/product.dart'; +import '../../../../services/auth_service.dart'; class CartController extends GetxController { - //TODO: Implement CartController + final RxList cartItems = [].obs; + final RxDouble totalPrice = 0.0.obs; - final count = 0.obs; @override void onInit() { super.onInit(); @@ -19,5 +21,51 @@ class CartController extends GetxController { super.onClose(); } - void increment() => count.value++; + void increment() => totalPrice.value++; + + Future signInAnonymously() async { + await AuthService.to.signInAnonymously(); + update(); + } + + Future convertAnonymousAccount(String email, String password) async { + await AuthService.to.convertAnonymousAccount(email, password); + } + + Future addItemToCart(Product product) async { + if (AuthService.to.user == null) { + // If the user is not signed in, sign them in anonymously + await signInAnonymously(); + } + cartItems.add(product); + totalPrice.value += product.price; + print("Product added to cart: ${product.name}"); + update(); + } + + Future removeItemFromCart(Product product) async { + cartItems.remove(product); + totalPrice.value -= product.price; + print("Product removed from cart: ${product.name}"); + update(); + } + + Future checkout() async { + if (AuthService.to.user == null) { + // If the user is not signed in, sign them in anonymously + await signInAnonymously(); + } + // Proceed with the checkout process + print("Checkout process initiated"); + update(); + } + + void calculateTotalPrice() { + totalPrice.value = cartItems.fold(0, (sum, item) => sum + item.price); + } + + void clearCart() { + cartItems.clear(); + calculateTotalPrice(); + } } diff --git a/lib/app/modules/cart/views/cart_view.dart b/lib/app/modules/cart/views/cart_view.dart index 3e048c79..468ce8fd 100644 --- a/lib/app/modules/cart/views/cart_view.dart +++ b/lib/app/modules/cart/views/cart_view.dart @@ -1,27 +1,102 @@ import 'package:flutter/material.dart'; - import 'package:get/get.dart'; -import 'package:get_flutter_fire/app/routes/app_pages.dart'; -import '../../../widgets/screen_widget.dart'; -import '../../../../services/auth_service.dart'; import '../controllers/cart_controller.dart'; +import 'package:get_flutter_fire/models/product.dart'; // Ensure correct path class CartView extends GetView { const CartView({super.key}); + @override Widget build(BuildContext context) { - return ScreenWidget( + return Scaffold( appBar: AppBar( - title: Text('${AuthService.to.userName} Cart'), + title: const Text('Cart'), centerTitle: true, ), - body: const Center( - child: Text( - 'CartView is working', - style: TextStyle(fontSize: 20), - ), - ), - screen: screen!, + body: Obx(() { + if (controller.cartItems.isEmpty) { + return const Center( + child: Text( + 'Your cart is empty', + style: TextStyle(fontSize: 20), + ), + ); + } + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListView.builder( + itemCount: controller.cartItems.length, + itemBuilder: (context, index) { + final item = controller.cartItems[index]; + return Card( + child: ListTile( + title: Text(item.name), + subtitle: + Text('Price: \$${item.price.toStringAsFixed(2)}'), + trailing: IconButton( + icon: const Icon(Icons.remove_circle), + onPressed: () { + _confirmRemoveItem(context, item); + }, + ), + ), + ); + }, + ), + ), + const SizedBox(height: 20), + Text( + 'Total: \$${controller.totalPrice.toStringAsFixed(2)}', + style: + const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + if (controller.cartItems.isNotEmpty) { + Get.toNamed('/checkout'); + } else { + Get.snackbar('Cart is empty', + 'Add items before proceeding to checkout'); + } + }, + child: const Text('Proceed to Checkout'), + ), + ], + ), + ); + }), + ); + } + + void _confirmRemoveItem(BuildContext context, Product item) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Remove Item'), + content: const Text( + 'Are you sure you want to remove this item from the cart?'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + controller.removeItemFromCart(item); + Get.back(); + }, + child: const Text('Remove'), + ), + ], + ); + }, ); } } diff --git a/lib/app/modules/checkout/bindings/checkout_binding.dart b/lib/app/modules/checkout/bindings/checkout_binding.dart index 42202b56..719208ce 100644 --- a/lib/app/modules/checkout/bindings/checkout_binding.dart +++ b/lib/app/modules/checkout/bindings/checkout_binding.dart @@ -1,5 +1,4 @@ import 'package:get/get.dart'; - import '../controllers/checkout_controller.dart'; class CheckoutBinding extends Bindings { diff --git a/lib/app/modules/checkout/controllers/checkout_controller.dart b/lib/app/modules/checkout/controllers/checkout_controller.dart index aa1265f6..bebeadd4 100644 --- a/lib/app/modules/checkout/controllers/checkout_controller.dart +++ b/lib/app/modules/checkout/controllers/checkout_controller.dart @@ -1,9 +1,10 @@ import 'package:get/get.dart'; +import '../../cart/controllers/cart_controller.dart'; class CheckoutController extends GetxController { - //TODO: Implement CheckoutController - + final cartController = Get.find(); final count = 0.obs; + @override void onInit() { super.onInit(); @@ -20,4 +21,19 @@ class CheckoutController extends GetxController { } void increment() => count.value++; + + double get totalPrice => cartController.totalPrice.value; + + void checkout() { + if (cartController.cartItems.isEmpty) { + print("Cart is empty. Add items before checking out."); + return; + } + // Placeholder for the payment process logic + print( + "Checkout initiated with total price: \$${totalPrice.toStringAsFixed(2)}"); + + // Clear the cart after successful checkout + cartController.clearCart(); + } } diff --git a/lib/app/modules/checkout/views/checkout_view.dart b/lib/app/modules/checkout/views/checkout_view.dart index b8b17072..d7c28b43 100644 --- a/lib/app/modules/checkout/views/checkout_view.dart +++ b/lib/app/modules/checkout/views/checkout_view.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; - import 'package:get/get.dart'; - import '../controllers/checkout_controller.dart'; class CheckoutView extends GetView { const CheckoutView({super.key}); + @override Widget build(BuildContext context) { return Scaffold( @@ -13,10 +12,23 @@ class CheckoutView extends GetView { title: const Text('CheckoutView'), centerTitle: true, ), - body: const Center( - child: Text( - 'CheckoutView is working', - style: TextStyle(fontSize: 20), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Total Price: \$${controller.totalPrice.toStringAsFixed(2)}', + style: const TextStyle(fontSize: 24), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + controller.checkout(); + }, + child: const Text('Checkout'), + ), + ], ), ), ); diff --git a/lib/app/modules/dashboard/controllers/dashboard_controller.dart b/lib/app/modules/dashboard/controllers/dashboard_controller.dart index 24d91a16..4a4e65d1 100644 --- a/lib/app/modules/dashboard/controllers/dashboard_controller.dart +++ b/lib/app/modules/dashboard/controllers/dashboard_controller.dart @@ -1,12 +1,17 @@ import 'dart:async'; - import 'package:get/get.dart'; class DashboardController extends GetxController { final now = DateTime.now().obs; + + final RxList> products = >[].obs; + final RxList> trendingProducts = >[].obs; + @override - void onReady() { - super.onReady(); + void onInit() { + super.onInit(); + fetchProducts(); + fetchTrendingProducts(); Timer.periodic( const Duration(seconds: 1), (timer) { @@ -14,4 +19,25 @@ class DashboardController extends GetxController { }, ); } + + void fetchProducts() { + products.assignAll([ + {'name': 'Smartphone', 'image': 'https://media.designrush.com/tinymce_images/341786/conversions/elin-content.jpg'}, + {'name': 'Laptop', 'image': 'https://media.designrush.com/tinymce_images/341786/conversions/elin-content.jpg'}, + {'name': 'Headphones', 'image': 'https://media.designrush.com/tinymce_images/341786/conversions/elin-content.jpg'}, + {'name': 'Smartwatch', 'image': 'https://media.designrush.com/tinymce_images/341786/conversions/elin-content.jpg'}, + {'name': 'Camera', 'image': 'https://media.designrush.com/tinymce_images/341786/conversions/elin-content.jpg'}, + {'name': 'Speaker', 'image': 'https://media.designrush.com/tinymce_images/341786/conversions/elin-content.jpg'}, + ]); + } + + void fetchTrendingProducts() { + trendingProducts.assignAll([ + {'name': 'Trending Product 1', 'image': 'https://ecommercephotographyindia.com/info/wp-content/uploads/2021/09/product-lifestyle-photography.jpg'}, + {'name': 'Trending Product 2', 'image': 'https://ecommercephotographyindia.com/info/wp-content/uploads/2021/09/product-lifestyle-photography.jpg'}, + {'name': 'Trending Product 3', 'image': 'https://ecommercephotographyindia.com/info/wp-content/uploads/2021/09/product-lifestyle-photography.jpg'}, + {'name': 'Trending Product 4', 'image': 'https://ecommercephotographyindia.com/info/wp-content/uploads/2021/09/product-lifestyle-photography.jpg'}, + {'name': 'Trending Product 5', 'image': 'https://ecommercephotographyindia.com/info/wp-content/uploads/2021/09/product-lifestyle-photography.jpg'}, + ]); + } } diff --git a/lib/app/modules/dashboard/views/dashboard_view.dart b/lib/app/modules/dashboard/views/dashboard_view.dart index f475030f..e62134a0 100644 --- a/lib/app/modules/dashboard/views/dashboard_view.dart +++ b/lib/app/modules/dashboard/views/dashboard_view.dart @@ -1,25 +1,232 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:carousel_slider/carousel_slider.dart'; import '../controllers/dashboard_controller.dart'; class DashboardView extends GetView { - const DashboardView({super.key}); + const DashboardView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( - body: Center( - child: Obx( - () => Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'DashboardView is working', - style: TextStyle(fontSize: 20), + body: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.network( + 'https://marketplace.canva.com/EAFMpwTmiRI/1/0/800w/canva-cream-elegant-fashion-sale-email-header-AR3z0NO4h7U.jpg', + width: double.infinity, + height: 150, + fit: BoxFit.contain, + ), + const SizedBox(height: 20), + // Trending Products Section + _buildTrendingProducts(), + const SizedBox(height: 20), + _buildCarousel(), + const SizedBox(height: 20), + const Padding( + padding: EdgeInsets.all(10.0), + child: Wrap( + spacing: 8, + children: [ + Chip( + label: Text('Category 1'), + ), + Chip( + label: Text('Category 2'), + ), + Chip( + label: Text('Category 3'), + ), + ], ), - Text('Time: ${controller.now.value.toString()}'), - ], + ), + // Product List + Obx( + () => ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: controller.products.length, + itemBuilder: (context, index) { + return _buildProductCard(controller.products[index]); + }, + ), + ), + ], + ), + ), + ); + } + + Widget _buildTrendingProducts() { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Trending Products', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 10), + Container( + height: 255, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: controller.trendingProducts.length, + itemBuilder: (context, index) { + return _buildTrendingProductCard(controller.trendingProducts[index]); + }, + ), + ), + ], + ), + ); + } + + Widget _buildTrendingProductCard(Map product) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + elevation: 4, + margin: EdgeInsets.symmetric(horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 150, + width: 180, + child: Image.network( + product['image']!, + fit: BoxFit.contain, + ), + ), + Padding( + padding: EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product['name']!, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + '\$ Product Price', + style: TextStyle( + fontSize: 14, + color: Colors.green, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 4), + Row( + children: [ + Icon(Icons.star, color: Colors.yellow), + Icon(Icons.star, color: Colors.yellow), + Icon(Icons.star, color: Colors.yellow), + Icon(Icons.star_border, color: Colors.yellow), + Icon(Icons.star_border, color: Colors.yellow), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildCarousel() { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Featured Products', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 11), + CarouselSlider( + options: CarouselOptions( + // height: 200.0, + enlargeCenterPage: true, + autoPlay: true, + aspectRatio: 16 / 9, + autoPlayCurve: Curves.fastOutSlowIn, + enableInfiniteScroll: true, + autoPlayAnimationDuration: Duration(milliseconds: 800), + viewportFraction: 0.8, + ), + items: controller.products.map((product) { + return Builder( + builder: (BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width, + margin: EdgeInsets.symmetric(horizontal: 5.0), + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(8.0), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.network( + product['image']!, + fit: BoxFit.cover, + height: 150, + width: double.infinity, + ), + Center( + child: Text( + product['name']!, + style: TextStyle(fontSize: 16.0), + ), + ), + ], + ), + ); + }, + ); + }).toList(), + ), + ], + ), + ); + } + + Widget _buildProductCard(Map product) { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Card( + elevation: 4, + margin: EdgeInsets.symmetric(vertical: 8), + child: ListTile( + leading: Image.network( + product['image']!, + width: 60, + height: 60, + fit: BoxFit.cover, + ), + title: Text(product['name']!), + subtitle: Text('\$ Product Price'), + trailing: ElevatedButton( + onPressed: () { + // Navigate to product details page or perform action + }, + child: Text('View'), ), ), ), diff --git a/lib/app/modules/home/views/home_view.dart b/lib/app/modules/home/views/home_view.dart index 0cfc040d..24006aeb 100644 --- a/lib/app/modules/home/views/home_view.dart +++ b/lib/app/modules/home/views/home_view.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:g_recaptcha_v3/g_recaptcha_v3.dart'; + import '../../../routes/app_pages.dart'; import '../../../widgets/screen_widget.dart'; import '../controllers/home_controller.dart'; @@ -9,6 +11,7 @@ class HomeView extends GetView { @override Widget build(BuildContext context) { + GRecaptchaV3.hideBadge(); return GetRouterOutlet.builder( builder: (context, delegate, currentRoute) { var arg = Get.rootDelegate.arguments(); diff --git a/lib/app/modules/login/controllers/login_controller.dart b/lib/app/modules/login/controllers/login_controller.dart index 5178fec9..2b6eee31 100644 --- a/lib/app/modules/login/controllers/login_controller.dart +++ b/lib/app/modules/login/controllers/login_controller.dart @@ -5,9 +5,10 @@ import '../../../../services/auth_service.dart'; class LoginController extends GetxController { static AuthService get to => Get.find(); - final Rx showReverificationButton = Rx(false); + // final Rx showReverificationButton = Rx(false); + var showReverificationButton = false.obs; - bool get isRobot => AuthService.to.robot.value == true; + bool get isRobot => AuthService.to.robot.value == false; set robot(bool v) => AuthService.to.robot.value = v; diff --git a/lib/app/modules/login/views/login_view.dart b/lib/app/modules/login/views/login_view.dart index 00c3af3f..1f4151d0 100644 --- a/lib/app/modules/login/views/login_view.dart +++ b/lib/app/modules/login/views/login_view.dart @@ -1,33 +1,17 @@ -// ignore_for_file: inference_failure_on_function_invocation - import 'package:firebase_auth/firebase_auth.dart' as fba; import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import '../../../../firebase_options.dart'; +import 'package:g_recaptcha_v3/g_recaptcha_v3.dart'; +import 'package:lottie/lottie.dart'; +import '../../../../firebase_options.dart'; import '../../../../models/screens.dart'; import '../../../widgets/login_widgets.dart'; import '../controllers/login_controller.dart'; class LoginView extends GetView { - void showReverificationButton( - bool show, fba.EmailAuthCredential? credential) { - // Below is very important. - // See [https://stackoverflow.com/questions/69351845/this-obx-widget-cannot-be-marked-as-needing-to-build-because-the-framework-is-al] - WidgetsBinding.instance.addPostFrameCallback((_) { - controller.showReverificationButton.value = show; - }); - //or Future.delayed(Duration.zero, () { - // We can get the email and password from the controllers either by making the whole screen from scratch - // or probably by add flutter_test find.byKey (hacky) - // tried using AuthStateChangeAction instead which is not getting called - // Finally Subclassed EmailAuthProvider to handle the same, but that also did not work - // So went for server side email sending option - //})); - } - const LoginView({super.key}); @override @@ -51,29 +35,37 @@ class LoginView extends GetView { Widget loginScreen(BuildContext context) { Widget ui; if (!controller.isLoggedIn) { + GRecaptchaV3.showBadge(); + ui = !(GetPlatform.isAndroid || GetPlatform.isIOS) && controller.isRobot - ? recaptcha() - : SignInScreen( - providers: [ - GoogleProvider(clientId: DefaultFirebaseOptions.webClientId), - MyEmailAuthProvider(), - ], - showAuthActionSwitch: !controller.isRegistered, - showPasswordVisibilityToggle: true, - headerBuilder: LoginWidgets.headerBuilder, - subtitleBuilder: subtitleBuilder, - footerBuilder: (context, action) => footerBuilder( - controller.showReverificationButton, - LoginController.to.credential), - sideBuilder: LoginWidgets.sideBuilder, - actions: getActions(), + ? recaptcha(context) + : SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SignInScreen( + providers: [ + GoogleProvider( + clientId: DefaultFirebaseOptions.webClientId), + MyEmailAuthProvider(), + ], + showAuthActionSwitch: !controller.isRegistered, + showPasswordVisibilityToggle: true, + headerBuilder: LoginWidgets.headerBuilder, + subtitleBuilder: subtitleBuilder, + footerBuilder: (context, action) => footerBuilder( + controller.showReverificationButton, + LoginController.to.credential), + sideBuilder: LoginWidgets.sideBuilder, + actions: getActions(), + ), + ], + ), ); } else if (controller.isAnon) { ui = RegisterScreen( - providers: [ - MyEmailAuthProvider(), - ], - showAuthActionSwitch: !controller.isAnon, //if Anon only SignUp + providers: [MyEmailAuthProvider()], + showAuthActionSwitch: !controller.isAnon, // If Anon only SignUp showPasswordVisibilityToggle: true, headerBuilder: LoginWidgets.headerBuilder, subtitleBuilder: subtitleBuilder, @@ -92,35 +84,48 @@ class LoginView extends GetView { return ui; } - Widget recaptcha() { - //TODO: Add Recaptcha + Widget recaptcha(BuildContext context) { return Scaffold( - body: TextButton( - onPressed: () => controller.robot = false, - child: const Text("Are you a Robot?"), - )); + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Lottie.asset( + 'lottie/tick.json', + width: 150, + height: 150, + ), + TextButton( + onPressed: () async { + String? token = await GRecaptchaV3.execute('login'); + print(token); + controller.robot = true; + Get.rootDelegate.toNamed(Screen.LOGIN.route); + }, + style: ButtonStyle( + side: MaterialStateProperty.all( + const BorderSide(color: Colors.blue, width: 2), + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), + ), + child: const Text("Are you a Robot?"), + ), + ], + ), + ), + ); } - /// The following actions are useful here: - /// - [AuthStateChangeAction] - /// - [AuthCancelledAction] - /// - [EmailLinkSignInAction] - /// - [VerifyPhoneAction] - /// - [SMSCodeRequestedAction] - List getActions() { return [ - // AuthStateChangeAction((context, state) { - AuthStateChangeAction((context, state) => LoginController.to - .errorMessage(context, state, showReverificationButton)), - // AuthStateChangeAction((context, state) { - // // This is not required due to the AuthMiddleware - // }), - // EmailLinkSignInAction((context) { - // final thenTo = Get.rootDelegate.currentConfiguration!.currentPage! - // .parameters?['then']; - // Get.rootDelegate.offNamed(thenTo ?? Routes.PROFILE); - // }), + AuthStateChangeAction((context, state) => + LoginController.to.errorMessage(context, state, (show, credential) { + controller.showReverificationButton.value = show; + })), ]; } } diff --git a/lib/app/modules/persona_selection_screen.dart b/lib/app/modules/persona_selection_screen.dart new file mode 100644 index 00000000..6488ad18 --- /dev/null +++ b/lib/app/modules/persona_selection_screen.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../services/persona_service.dart'; + +class PersonaSelectionScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + final personaService = Get.find(); + + return Scaffold( + appBar: AppBar( + title: Text('Select Persona'), + ), + body: GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 10.0, + mainAxisSpacing: 10.0, + childAspectRatio: 1.2, + ), + itemCount: personaService.personas.length, + itemBuilder: (context, index) { + final persona = personaService.personas[index]; + + return GestureDetector( + onTap: () { + personaService.selectPersona(persona); + Get.back(); + }, + child: Card( + elevation: 3.0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircleAvatar( + radius: 50.0, + backgroundImage: NetworkImage(persona.imageUrl), + ), + SizedBox(height: 10.0), + Text( + persona.name, + style: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/app/modules/product_details/controllers/product_details_controller.dart b/lib/app/modules/product_details/controllers/product_details_controller.dart index d894e10c..3ecc4359 100644 --- a/lib/app/modules/product_details/controllers/product_details_controller.dart +++ b/lib/app/modules/product_details/controllers/product_details_controller.dart @@ -1,13 +1,39 @@ import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; class ProductDetailsController extends GetxController { final String productId; + final String apiUrl = 'https://anubhav-website.onrender.com/api/product-details'; + var productDetails = {}.obs; + var isLoading = true.obs; ProductDetailsController(this.productId); + @override void onInit() { super.onInit(); - Get.log('ProductDetailsController created with id: $productId'); + fetchProductDetails(); + } + + Future fetchProductDetails() async { + final response = await http.post( + Uri.parse(apiUrl), + body: json.encode({'productId': productId}), + headers: {'Content-Type': 'application/json'}, + ); + + if (response.statusCode == 200) { + final responseData = json.decode(response.body); + if (responseData['success']) { + productDetails.value = responseData['data']; + } else { + Get.snackbar('Error', responseData['message'] ?? 'Failed to fetch product details'); + } + } else { + Get.snackbar('Error', 'Failed to fetch product details'); + } + isLoading.value = false; } @override diff --git a/lib/app/modules/product_details/views/product_details_view.dart b/lib/app/modules/product_details/views/product_details_view.dart index c9290724..035d9be7 100644 --- a/lib/app/modules/product_details/views/product_details_view.dart +++ b/lib/app/modules/product_details/views/product_details_view.dart @@ -1,7 +1,6 @@ +import 'dart:async'; import 'package:flutter/material.dart'; - import 'package:get/get.dart'; - import '../controllers/product_details_controller.dart'; class ProductDetailsView extends GetWidget { @@ -9,19 +8,272 @@ class ProductDetailsView extends GetWidget { @override Widget build(BuildContext context) { + final PageController pageController = PageController(); + return Scaffold( - body: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - 'ProductDetailsView is working', - style: TextStyle(fontSize: 20), + body: Obx(() { + if (controller.isLoading.value) { + return Center(child: CircularProgressIndicator()); + } + + if (controller.productDetails.isEmpty) { + return Center( + child: Text( + 'Failed to load product details', + style: TextStyle(fontSize: 16, color: Colors.red), + ), + ); + } + + final product = controller.productDetails; + final productImages = (product['productImage'] as List?) ?? []; + final price = product['price']?.toDouble() ?? 0.0; + final sellingPrice = product['sellingPrice']?.toDouble() ?? 0.0; + final discountPercentage = + price == 0.0 ? '0.0' : ((price - sellingPrice) / price * 100).toStringAsFixed(1); + Timer.periodic(Duration(seconds: 3), (Timer timer) { + if (pageController.hasClients) { + int nextPage = (pageController.page?.round() ?? 0) + 1; + if (nextPage == productImages.length) { + nextPage = 0; + } + pageController.animateToPage( + nextPage, + duration: Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + } + }); + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Product Images Carousel + Container( + height: 300, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + spreadRadius: 2, + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Stack( + children: [ + PageView.builder( + controller: pageController, + itemCount: productImages.length, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.network( + productImages[index], + fit: BoxFit.cover, + width: double.infinity, + ), + ), + ); + }, + scrollDirection: Axis.horizontal, + ), + Positioned( + bottom: 16, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + productImages.length, + (index) => Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + width: 8, + height: 8, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.white.withOpacity(index == 0 ? 1 : 0.5), + ), + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + // Product Details + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + product['productName'] ?? 'N/A', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ), + if (discountPercentage != "0.0") + Container( + padding: EdgeInsets.symmetric(horizontal: 4, vertical: 4), + decoration: BoxDecoration( + color: Colors.redAccent, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '$discountPercentage% OFF', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + 'Brand: ', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + Text( + product['brandName'] ?? 'N/A', + style: TextStyle( + fontSize: 14, + color: Colors.black87, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Text( + 'Category: ', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + Text( + product['category'] ?? 'N/A', + style: TextStyle( + fontSize: 14, + color: Colors.black87, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 12), + Text( + 'Description:', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + const SizedBox(height: 8), + Text( + product['description'] ?? 'No description available', + style: TextStyle( + fontSize: 14, + height: 1.5, + color: Colors.grey[700], + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Text( + 'Price: ', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + Text( + '\$${price.toStringAsFixed(2)}', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black87, + decoration: TextDecoration.lineThrough, + ), + ), + const SizedBox(width: 8), + Text( + '\$${sellingPrice.toStringAsFixed(2)}', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.green, + ), + ), + ], + ), + const SizedBox(height: 16), + // Add to Cart Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.redAccent, + padding: EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + 'Add to Cart', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ), + const SizedBox(height: 12), + // Buy Now Button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + padding: EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + 'Buy Now', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ), + ], ), - Text('ProductId: ${controller.productId}') - ], - ), - ), + ), + ); + }), ); } } diff --git a/lib/app/modules/products/controllers/products_controller.dart b/lib/app/modules/products/controllers/products_controller.dart index 118c7dc8..0836af99 100644 --- a/lib/app/modules/products/controllers/products_controller.dart +++ b/lib/app/modules/products/controllers/products_controller.dart @@ -1,23 +1,38 @@ import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; import '../../../../models/product.dart'; class ProductsController extends GetxController { final products = [].obs; + final apiUrl = 'https://anubhav-website.onrender.com/api/get-product'; - void loadDemoProductsFromSomeWhere() { - products.add( - Product( - name: 'Product added on: ${DateTime.now().toString()}', - id: DateTime.now().millisecondsSinceEpoch.toString(), - ), - ); + @override + void onInit() { + super.onInit(); + fetchProductsFromApi(); } - @override - void onReady() { - super.onReady(); - loadDemoProductsFromSomeWhere(); + Future fetchProductsFromApi() async { + try { + final response = await http.get(Uri.parse(apiUrl)); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data['success']) { + products.value = List.from( + data['data'].map((product) => Product.fromJson(product)), + ); + } else { + print('Failed to load data: ${response.statusCode}'); + } + } else { + print('Failed to load data: ${response.statusCode}'); + } + } catch (e) { + print(e); + Get.snackbar('Error', 'Failed to fetch products'); + } } @override diff --git a/lib/app/modules/products/views/products_view.dart b/lib/app/modules/products/views/products_view.dart index 5b190a6a..d4dc7f02 100644 --- a/lib/app/modules/products/views/products_view.dart +++ b/lib/app/modules/products/views/products_view.dart @@ -1,5 +1,3 @@ -// ignore_for_file: inference_failure_on_function_invocation - import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -12,42 +10,95 @@ class ProductsView extends GetView { @override Widget build(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + controller.fetchProductsFromApi(); + }); + var arg = Get.rootDelegate.arguments(); return Scaffold( floatingActionButton: (arg != null && Get.rootDelegate.arguments()["role"] == Role.seller) ? FloatingActionButton.extended( - onPressed: controller.loadDemoProductsFromSomeWhere, - label: const Text('Add'), + onPressed: controller.fetchProductsFromApi, + label: const Text('Refresh'), + icon: const Icon(Icons.refresh), ) : null, body: Column( children: [ - const Hero( - tag: 'heroLogo', - child: FlutterLogo(), - ), Expanded( child: Obx( () => RefreshIndicator( onRefresh: () async { - controller.products.clear(); - controller.loadDemoProductsFromSomeWhere(); + await controller.fetchProductsFromApi(); }, - child: ListView.builder( - itemCount: controller.products.length, - itemBuilder: (context, index) { - final item = controller.products[index]; - return ListTile( - onTap: () { - Get.rootDelegate.toNamed(Routes.PRODUCT_DETAILS( - item.id)); //we could use Get Parameters - }, - title: Text(item.name), - subtitle: Text(item.id), - ); - }, - ), + child: controller.products.isEmpty + ? const Center(child: CircularProgressIndicator()) + : ListView.builder( + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 16), + itemCount: controller.products.length, + itemBuilder: (context, index) { + final product = controller.products[index]; + return Container( + margin: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: Color.fromARGB(255, 255, 255, 255), + border: Border.all( + color: const Color.fromARGB(255, 0, 0, 0), + width: 1), + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + spreadRadius: 2, + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ListTile( + contentPadding: const EdgeInsets.all(16), + onTap: () { + Get.rootDelegate.toNamed( + Routes.PRODUCT_DETAILS(product.id)); + }, + title: Text( + product.name, + style: const TextStyle( + fontWeight: FontWeight.bold, fontSize: 16), + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '${product.brandName}\n${product.category.isNotEmpty ? product.category : 'No Category'}', + style: const TextStyle( + color: Colors.grey, fontSize: 14), + ), + ), + leading: product.productImage.isNotEmpty + ? CircleAvatar( + backgroundImage: + NetworkImage(product.productImage), + radius: 30, + ) + : const CircleAvatar( + backgroundColor: Colors.grey, + radius: 30, + child: Icon(Icons.image_not_supported, + size: 30, color: Colors.white), + ), + trailing: Text( + '\$${product.price.toStringAsFixed(2)}', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Colors.green), + ), + ), + ); + }, + ), ), ), ), diff --git a/lib/app/modules/profile/controllers/profile_controller.dart b/lib/app/modules/profile/controllers/profile_controller.dart index 0c1e059e..e1dc9d34 100644 --- a/lib/app/modules/profile/controllers/profile_controller.dart +++ b/lib/app/modules/profile/controllers/profile_controller.dart @@ -1,10 +1,8 @@ import 'dart:io'; - import 'package:firebase_auth/firebase_auth.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:firebase_storage/firebase_storage.dart'; import 'package:get/get.dart'; -import 'package:get_storage/get_storage.dart'; - import 'package:path/path.dart'; import '../../../../services/auth_service.dart'; @@ -13,50 +11,67 @@ class ProfileController extends GetxController { User? currentUser = AuthService.to.user; final Rxn _photoURL = Rxn(); - File? _photo; - String? get photoURL => _photoURL.value; @override - onInit() { + void onInit() { super.onInit(); - _photoURL.value = currentUser!.photoURL; + _photoURL.value = currentUser?.photoURL; _photoURL.bindStream(currentUser!.photoURL.obs.stream); } + Future pickImage() async { + final ImagePicker _picker = ImagePicker(); + final pickedFile = await _picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + await uploadFile(pickedFile.path); + } + } + Future uploadFile(String path) async { try { - var byt = GetStorage().read(path); - if (byt != null) { - final fileName = path; - final destination = 'profilePics/${currentUser!.uid}'; - - final ref = storage.ref(destination).child(fileName); - await ref.putData(byt); - return "$destination/$fileName"; + if (path.isEmpty) { + Get.snackbar('Error', 'Invalid file path'); + return null; + } + + File fileToUpload = File(path); + bool fileExists = await fileToUpload.exists(); + print('Path: $path, File Exists: $fileExists'); + + if (fileExists) { + final fileName = basename(fileToUpload.path); + final destination = 'profilePics/${currentUser!.uid}/$fileName'; + final ref = storage.ref(destination); + + await ref.putFile(fileToUpload); + final downloadURL = await ref.getDownloadURL(); + await updatePhotoURL(downloadURL); + return downloadURL; } else { - _photo = File(path); - if (_photo == null) return null; - final fileName = basename(_photo!.path); - final destination = 'profilePics/${currentUser!.uid}'; - - final ref = storage.ref(destination).child(fileName); - await ref.putFile(_photo!); - return "$destination/$fileName"; + Get.snackbar('Error', 'File does not exist at $path'); + return null; } + } catch (e, stacktrace) { + print('Error uploading file: $e'); + print('Stacktrace: $stacktrace'); + Get.snackbar('Error', 'Image Not Uploaded: ${e.toString()}'); + return null; + } + } + + Future updatePhotoURL(String url) async { + try { + _photoURL.value = url; + await currentUser?.updatePhotoURL(url); + Get.snackbar('Success', 'Profile picture updated successfully'); } catch (e) { - Get.snackbar('Error', 'Image Not Uploaded as ${e.toString()}'); + print('Error updating photo URL: $e'); + Get.snackbar('Error', 'Failed to update photo URL: ${e.toString()}'); } - return null; } void logout() { AuthService.to.logout(); } - - Future updatePhotoURL(String dest) async { - _photoURL.value = await storage.ref().child(dest).getDownloadURL(); - await currentUser?.updatePhotoURL(_photoURL.value); - Get.snackbar('Success', 'Picture stored and linked'); - } } diff --git a/lib/app/modules/profile/views/profile_view.dart b/lib/app/modules/profile/views/profile_view.dart index c26d11c1..af1ebbff 100644 --- a/lib/app/modules/profile/views/profile_view.dart +++ b/lib/app/modules/profile/views/profile_view.dart @@ -1,11 +1,7 @@ -// ignore_for_file: inference_failure_on_function_invocation - -import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../../../services/auth_service.dart'; -import '../../../../models/screens.dart'; import '../../../widgets/change_password_dialog.dart'; import '../../../widgets/image_picker_button.dart'; import '../controllers/profile_controller.dart'; @@ -25,7 +21,6 @@ class ProfileView extends GetView { if (frame == null) { return Container(color: placeholderColor); } - return child!; } @@ -36,80 +31,137 @@ class ProfileView extends GetView { Widget profileScreen() { return AuthService.to.isLoggedInValue - ? ProfileScreen( - // We are using the Flutter Fire Profile Screen now but will change in subsequent steps. - // The issues are highlighted in comments here - - // appBar: AppBar( - // title: const Text('User Profile'), - // ), - avatar: SizedBox( - //null will give the profile image component but it does not refresh the pic when changed - height: size, - width: size, - child: ClipPath( - clipper: ShapeBorderClipper(shape: shape), - clipBehavior: Clip.hardEdge, - child: controller.photoURL != null + ? Scaffold( + appBar: AppBar( + title: const Text('Profile'), + actions: [ + if (Theme.of(Get.context!).platform == TargetPlatform.android || + Theme.of(Get.context!).platform == TargetPlatform.iOS) + IconButton( + icon: const Icon(Icons.camera_alt), + onPressed: () { + _showBottomSheet(Get.context!, controller); + }, + ) + else + PopupMenuButton( + onSelected: (value) { + if (value == 'Select Image') { + controller.pickImage(); + } + }, + itemBuilder: (BuildContext context) { + return ['Select Image'].map((String choice) { + return PopupMenuItem( + value: choice, + child: Text(choice), + ); + }).toList(); + }, + ), + ], + ), + body: Center( + child: Obx(() { + return controller.photoURL != null && + controller.photoURL!.isNotEmpty ? Image.network( controller.photoURL!, width: size, height: size, - cacheWidth: size.toInt(), - cacheHeight: size.toInt(), fit: BoxFit.contain, frameBuilder: _imageFrameBuilder, + errorBuilder: (context, error, stackTrace) { + return Center( + child: Image.asset( + 'assets/images/dash.png', + width: size, + fit: BoxFit.contain, + ), + ); + }, ) - : Center( - child: Image.asset( - 'assets/images/dash.png', - width: size, - fit: BoxFit.contain, - ), - ), + : Image.asset( + 'assets/images/dash.png', + width: size, + fit: BoxFit.contain, + ); + }), + ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (controller.currentUser?.email != null) + TextButton.icon( + onPressed: () => _resetPasswordEmailVerification(), + label: const Text('Reset Password'), + icon: const Icon(Icons.email_rounded), + ), + ImagePickerButton(callback: (String? path) async { + if (path != null) { + String? dest = await controller.uploadFile(path); + if (dest != null) { + await controller.updatePhotoURL(dest); + } + } + }), + ], ), ), - // showDeleteConfirmationDialog: true, //this does not work properly. Possibly a bug in FlutterFire - actions: [ - SignedOutAction((context) { - Get.back(); - controller.logout(); - Get.rootDelegate.toNamed(Screen.PROFILE.route); - // Navigator.of(context).pop(); - }), - AccountDeletedAction((context, user) { - //If we don't include this the button is still shown but no action gets done. Ideally the button should also not be shown. Its a bug in FlutterFire - Get.defaultDialog( - //this is only called after the delete is done and not useful for confirmation of the delete action - title: 'Deleted Account of ${user.displayName}', - barrierDismissible: true, - navigatorKey: Get.nestedKey(Screen.HOME.route), - ); - }) - ], - children: [ - //This is to show that we can add custom content here - const Divider(), - controller.currentUser?.email != null - ? TextButton.icon( - onPressed: callChangePwdDialog, - label: const Text('Change Password'), - icon: const Icon(Icons.password_rounded), - ) - : const SizedBox.shrink(), - ImagePickerButton(callback: (String? path) async { - if (path != null) { - //Upload to Store - String? dest = await controller.uploadFile(path); - //attach it to User imageUrl - if (dest != null) { - await controller.updatePhotoURL(dest); - } - } - }) - ], + floatingActionButton: FloatingActionButton( + onPressed: () { + callChangePwdDialog(); + }, + child: const Icon(Icons.lock), + ), ) - : const Scaffold(); + : Scaffold(); + } + + void _showBottomSheet(BuildContext context, ProfileController controller) { + showModalBottomSheet( + context: context, + builder: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo_library), + title: Text('Select from gallery'), + onTap: () { + controller.pickImage(); + Navigator.pop(context); + }, + ), + ListTile( + leading: Icon(Icons.camera_alt), + title: Text('Take a photo'), + onTap: () { + controller + .pickImage(); // Adjust if you want to add camera option + Navigator.pop(context); + }, + ), + ], + ); + }, + ); + } + + void _resetPasswordEmailVerification() async { + final email = controller.currentUser?.email; + if (email != null) { + await AuthService.to.sendPasswordResetEmail(email); + } else { + Get.snackbar( + 'Error', + 'No email address found for the current user.', + snackPosition: SnackPosition.BOTTOM, + duration: Duration(seconds: 3), + ); + } } void callChangePwdDialog() { diff --git a/lib/app/modules/root/views/drawer.dart b/lib/app/modules/root/views/drawer.dart index 908d0223..b73c8957 100644 --- a/lib/app/modules/root/views/drawer.dart +++ b/lib/app/modules/root/views/drawer.dart @@ -3,47 +3,159 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import '../../../../services/persona_service.dart'; +import '../../../../utils/size_config.dart'; import '../../../../models/role.dart'; import '../../../../services/auth_service.dart'; - import '../../../../models/screens.dart'; import '../controllers/my_drawer_controller.dart'; class DrawerWidget extends StatelessWidget { - const DrawerWidget({ - super.key, - }); + const DrawerWidget({super.key}); @override Widget build(BuildContext context) { - MyDrawerController controller = Get.put(MyDrawerController([]), - permanent: true); //must make true else gives error + MyDrawerController controller = + Get.put(MyDrawerController([]), permanent: true); Screen.drawer().then((v) => {controller.values.value = v}); - return Obx(() => Drawer( - //changing the shape of the drawer - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(0), bottomRight: Radius.circular(20)), - ), - width: 200, - child: Column( - children: drawerItems(context, controller.values), - ), - )); + SizeConfig.init(context); + double drawerWidth = + GetPlatform.isDesktop ? SizeConfig.w * 0.27 : SizeConfig.w * 0.7; + + return Obx(() { + Role userRole = AuthService.to.maxRole; + Persona selectedPersona = Get.find().selectedPersona; + return LayoutBuilder( + builder: (context, constraints) { + bool isHorizontal = GetPlatform.isDesktop; + + // Determine the width for the left-side navigation panel + double leftPanelWidth = + isHorizontal ? SizeConfig.w * 0.16 : drawerWidth; + + return Row( + children: [ + // Extreme left vertical navigation strip + if (isHorizontal) + Container( + width: SizeConfig.w * 0.08, // Adjust width as needed + color: Colors.blueGrey, + child: Column( + // Vertical navigation items can be added here + children: [ + SizedBox(height: SizeConfig.h * 0.1), + IconButton( + icon: const Icon(Icons.dashboard), + onPressed: () { + // Handle navigation or actions + Navigator.of(context).pop(); + }, + ), + if (GetPlatform.isDesktop) + ...userRole.tabs.map((tab) => Padding( + padding: const EdgeInsets.only(top: 8.0), + child: IconButton( + icon: Icon(tab.icon), + onPressed: () { + Get.rootDelegate.toNamed(tab.route); + Navigator.of(context).pop(); + }, + ), + )), + ], + ), + ), + // Left-side navigation panel (drawer) + SizedBox( + width: leftPanelWidth, + child: Drawer( + backgroundColor: selectedPersona.drawerColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + ), + child: Column( + children: drawerItems( + context, controller.values, selectedPersona), + ), + ), + ), + ], + ); + }, + ); + }); } - List drawerItems(BuildContext context, Rx> values) { + List drawerItems(BuildContext context, Rx> values, + Persona selectedPersona) { + double sWidth = + GetPlatform.isDesktop ? SizeConfig.w * 0.25 : SizeConfig.w * 0.7; List list = [ Container( - height: 100, - color: Colors.red, - //adding content in the highlighted part of the drawer + height: SizeConfig.h * 0.37, + color: selectedPersona.drawerColor, child: Align( - alignment: Alignment.centerLeft, - child: Container( - margin: const EdgeInsets.only(left: 15), - child: const Text('User Name', //Profile Icon also - style: TextStyle(fontWeight: FontWeight.bold)))), + alignment: Alignment.centerLeft, + child: Container( + + margin: EdgeInsets.only( + left: SizeConfig.h * 0.02, + top: SizeConfig.h * 0.02, + right: SizeConfig.h * 0.02), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(50)), + color: selectedPersona.drawerColor1), + child: SizedBox( + height: sWidth, + width: sWidth, + child: ListView( + children: [ + const SizedBox(height: 10), + CircleAvatar( + radius: 50.5, + backgroundColor: Colors.black, + child: CircleAvatar( + radius: 50, + backgroundImage: + NetworkImage(AuthService.to.userPhotoUrl ?? ''), + ), + ), + Center( + child: Container( + padding: EdgeInsets.only(top: SizeConfig.h * 0.01), + child: Text( + '${AuthService.to.userName}', + style: const TextStyle( + fontSize: 20, + ), + ), + ), + ), + Center( + child: Text( + AuthService.to.userEmail ?? '', + style: const TextStyle( + fontSize: 15, + color: Color.fromARGB(169, 37, 38, 39)), + ), + ), + const SizedBox(height: 10), + Center( + child: Text( + 'Selected Persona: ${selectedPersona.name}', + style: const TextStyle( + fontSize: 15, + color: Colors.blue, + fontWeight: FontWeight.bold), + ), + ), + ], + )), + ), + ), ) ]; @@ -51,6 +163,7 @@ class DrawerWidget extends StatelessWidget { for (var i = 0; i <= AuthService.to.maxRole.index; i++) { Role role = Role.values[i]; list.add(ListTile( + tileColor: selectedPersona.drawerColor, title: Text( role.name, style: const TextStyle( @@ -67,50 +180,85 @@ class DrawerWidget extends StatelessWidget { } } - for (Screen screen in values.value) { - list.add(ListTile( - title: Text(screen.label ?? ''), - onTap: () { - Get.rootDelegate.toNamed(screen.route); - //to close the drawer - - Navigator.of(context).pop(); + list.add( + Obx( + () { + return Column( + children: values.value.map((screen) { + return Padding( + padding: EdgeInsets.only(left: SizeConfig.h * 0.01), + child: ListTile( + tileColor:selectedPersona.drawerColor, + leading: Icon( + screen.icon, + + ), + title: Text( + screen.label ?? '', + style: const TextStyle( + fontWeight: FontWeight.w400), + ), + onTap: () { + Get.rootDelegate.toNamed(screen.route); + Navigator.of(context).pop(); + }, + ), + ); + }).toList(), + ); }, - )); - } + ), + ); if (AuthService.to.isLoggedInValue) { - list.add(ListTile( - title: const Text( - 'Logout', - style: TextStyle( - color: Colors.red, + list.add(const Spacer(flex: 1)); + list.add( + Padding( + padding: EdgeInsets.only(left: SizeConfig.h * 0.01), + child: ListTile( + leading: const Icon( + Icons.logout_sharp, + color: Colors.black, + ), + title: const Text( + 'Logout', + style: TextStyle(color: Colors.red, fontWeight: FontWeight.w400), + ), + onTap: () { + AuthService.to.logout(); + Get.rootDelegate.toNamed(Screen.LOGIN.route); + //to close the drawer + Navigator.of(context).pop(); + }, ), ), - onTap: () { - AuthService.to.logout(); - Get.rootDelegate.toNamed(Screen.LOGIN.route); - //to close the drawer - - Navigator.of(context).pop(); - }, - )); + ); + list.add(SizedBox(height: SizeConfig.h * 0.02)); } if (!AuthService.to.isLoggedInValue) { - list.add(ListTile( - title: const Text( - 'Login', - style: TextStyle( - color: Colors.blue, + list.add(const Spacer(flex: 1)); + list.add( + Padding( + padding: EdgeInsets.only(left: SizeConfig.h * 0.01), + child: ListTile( + leading: const Icon( + Icons.login_sharp, + color: Colors.black, + ), + title: const Text( + 'Login', + style: TextStyle( + color: Colors.blue, + ), + ), + onTap: () { + Get.rootDelegate.toNamed(Screen.LOGIN.route); + //to close the drawer + Navigator.of(context).pop(); + }, ), ), - onTap: () { - Get.rootDelegate.toNamed(Screen.LOGIN.route); - //to close the drawer - - Navigator.of(context).pop(); - }, - )); + ); } return list; diff --git a/lib/app/modules/root/views/root_view.dart b/lib/app/modules/root/views/root_view.dart index 2bbf228c..fa5220fc 100644 --- a/lib/app/modules/root/views/root_view.dart +++ b/lib/app/modules/root/views/root_view.dart @@ -1,15 +1,16 @@ -// ignore_for_file: inference_failure_on_function_invocation - import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_flutter_fire/services/auth_service.dart'; import '../../../routes/app_pages.dart'; import '../../../../models/screens.dart'; +import '../../../utils/icon_constants.dart'; +import '../../../widgets/screen_widget.dart'; import '../controllers/root_controller.dart'; import 'drawer.dart'; class RootView extends GetView { - const RootView({super.key}); + RootView({super.key}); + final RxBool isSearchBarVisible = false.obs; @override Widget build(BuildContext context) { @@ -31,34 +32,109 @@ class RootView extends GetView { ) : IconButton( icon: ImageIcon( - const AssetImage("icons/logo.png"), + const AssetImage(IconConstants.logo), color: Colors.grey.shade800, ), onPressed: () => AuthService.to.isLoggedInValue ? controller.openDrawer() : {Screen.HOME.doAction()}, ), - actions: topRightMenuButtons(current), - // automaticallyImplyLeading: false, //removes drawer icon + actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + // Toggle search bar visibility + isSearchBarVisible.value = !isSearchBarVisible.value; + }, + ), + topRightMenuButtons(current), + ], ), - body: GetRouterOutlet( - initialRoute: AppPages.INITIAL, - // anchorRoute: '/', - // filterPages: (afterAnchor) { - // return afterAnchor.take(1); - // }, + body: Stack( + children: [ + GetRouterOutlet( + initialRoute: AppPages.INITIAL, + ), + Obx(() { + if (isSearchBarVisible.value) { + return buildSearchBar(); + } else { + return const SizedBox.shrink(); + } + }), + ], ), ); }, ); } -//This could be used to add icon buttons in expanded web view instead of the context menu - List topRightMenuButtons(GetNavConfig current) { - return [ - Container( - margin: const EdgeInsets.only(right: 15), - child: Screen.LOGIN.widget(current)) - ]; //TODO add seach button + Widget topRightMenuButtons(GetNavConfig current) { + return Container( + margin: const EdgeInsets.only(right: 15), + child: Screen.LOGIN.widget(current), + ); } + +Widget buildSearchBar() { + final TextEditingController searchController = TextEditingController(); + + return Positioned( + top: 0, + left: 10, + right: 10, + child: Obx(() { + return Visibility( + visible: isSearchBarVisible.value, + maintainState: true, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Colors.grey, + width: 1.0, + ), + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: searchController, + decoration: InputDecoration( + hintText: 'Search...', + border: InputBorder.none, + suffixIcon: IconButton( + icon: const Icon(Icons.clear, size: 20), + onPressed: () { + searchController.clear(); + print('Clear button pressed'); + }, + ), + ), + onChanged: (value) { + print('Typed: $value'); + }, + ), + ), + IconButton( + icon: const Icon(Icons.arrow_forward_outlined), + onPressed: () { + String searchText = searchController.text.trim(); + if (searchText.isNotEmpty) { + print('Performing search for: $searchText'); + isSearchBarVisible.value = false; + } else { + print('Search text is empty'); + } + }, + ), + ], + ), + ), + ); + }), + ); +} } diff --git a/lib/app/modules/search/bindings/search_binding.dart b/lib/app/modules/search/bindings/search_binding.dart new file mode 100644 index 00000000..64cfa36f --- /dev/null +++ b/lib/app/modules/search/bindings/search_binding.dart @@ -0,0 +1,12 @@ +import 'package:get/get.dart'; + +import '../controllers/search_controller.dart'; + +class SearchBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => SearchController(), + ); + } +} diff --git a/lib/app/modules/search/controllers/search_controller.dart b/lib/app/modules/search/controllers/search_controller.dart new file mode 100644 index 00000000..f282fabb --- /dev/null +++ b/lib/app/modules/search/controllers/search_controller.dart @@ -0,0 +1,23 @@ +import 'package:get/get.dart'; + +class SearchController extends GetxController { + //TODO: Implement SearchController + + final count = 0.obs; + @override + void onInit() { + super.onInit(); + } + + @override + void onReady() { + super.onReady(); + } + + @override + void onClose() { + super.onClose(); + } + + void increment() => count.value++; +} diff --git a/lib/app/modules/search/views/search_view.dart b/lib/app/modules/search/views/search_view.dart new file mode 100644 index 00000000..f0c3c16c --- /dev/null +++ b/lib/app/modules/search/views/search_view.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart' hide SearchController; + +import 'package:get/get.dart'; + +import '../controllers/search_controller.dart'; + +class SearchView extends GetView { + const SearchView({Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('SearchView'), + centerTitle: true, + ), + body: const Center( + child: Text( + 'SearchView is working', + style: TextStyle(fontSize: 20), + ), + ), + ); + } +} diff --git a/lib/app/modules/settings/views/settings_view.dart b/lib/app/modules/settings/views/settings_view.dart index 2bb244b6..a7d726ee 100644 --- a/lib/app/modules/settings/views/settings_view.dart +++ b/lib/app/modules/settings/views/settings_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; - import 'package:get/get.dart'; - +import '../../../../services/persona_service.dart'; +import '../../persona_selection_screen.dart'; import '../controllers/settings_controller.dart'; class SettingsView extends GetView { @@ -9,12 +9,30 @@ class SettingsView extends GetView { @override Widget build(BuildContext context) { - return const Scaffold( - body: Center( - child: Text( - 'SettingsView is working', - style: TextStyle(fontSize: 20), - ), + final personaService = Get.find(); + + return Scaffold( + body: ListView( + children: [ + Obx(() { + bool isDarkMode = personaService.themeMode == ThemeMode.dark; + return ListTile( + title: Text('Dark Mode'), + trailing: Switch( + value: isDarkMode, + onChanged: (value) { + personaService.toggleDarkMode(value); + }, + ), + ); + }), + ListTile( + title: Text('Select Persona'), + onTap: () { + Get.to(() => PersonaSelectionScreen()); + }, + ), + ], ), ); } diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index 7269755d..f1c10006 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; + import 'package:get/get.dart'; import '../../models/access_level.dart'; import '../../models/role.dart'; +import '../../models/screens.dart'; import '../middleware/auth_middleware.dart'; import '../modules/cart/bindings/cart_binding.dart'; import '../modules/cart/views/cart_view.dart'; @@ -28,6 +30,8 @@ import '../modules/register/bindings/register_binding.dart'; import '../modules/register/views/register_view.dart'; import '../modules/root/bindings/root_binding.dart'; import '../modules/root/views/root_view.dart'; +import '../modules/search/bindings/search_binding.dart'; +import '../modules/search/views/search_view.dart'; import '../modules/settings/bindings/settings_binding.dart'; import '../modules/settings/views/settings_view.dart'; import '../modules/task_details/bindings/task_details_binding.dart'; @@ -36,7 +40,6 @@ import '../modules/tasks/bindings/tasks_binding.dart'; import '../modules/tasks/views/tasks_view.dart'; import '../modules/users/bindings/users_binding.dart'; import '../modules/users/views/users_view.dart'; -import '../../models/screens.dart'; part 'app_routes.dart'; part 'screen_extension.dart'; @@ -51,7 +54,7 @@ class AppPages { static final routes = [ GetPage( name: '/', - page: () => const RootView(), + page: () => RootView(), binding: RootBinding(), participatesInRootNavigator: true, preventDuplicates: true, @@ -150,5 +153,9 @@ class AppPages { ) ], ), + Screen.SEARCH.getPage( + page: () => const SearchView(), + binding: SearchBinding(), + ), ]; } diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index f3129d21..7391dcc0 100644 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -30,6 +30,7 @@ abstract class Routes { '${Screen.LOGIN.route}?then=${Uri.encodeQueryComponent(afterSuccessfulLogin)}'; static String REGISTER_THEN(String afterSuccessfulLogin) => '${Screen.REGISTER.route}?then=${Uri.encodeQueryComponent(afterSuccessfulLogin)}'; + // static const SEARCH = _Paths.SEARCH; } // Keeping this as Get_Cli will require it. Any addition can later be added to Screen @@ -51,4 +52,5 @@ abstract class _Paths { // static const USERS = '/users'; // static const USER_PROFILE = '/:uId'; // static const MY_PRODUCTS = '/my-products'; + // static const SEARCH = '/search'; } diff --git a/lib/app/routes/screen_extension.dart b/lib/app/routes/screen_extension.dart index aaf138b0..23877c7d 100644 --- a/lib/app/routes/screen_extension.dart +++ b/lib/app/routes/screen_extension.dart @@ -110,11 +110,14 @@ extension ScreenExtension on Screen { extension RoleExtension on Role { int getCurrentIndexFromRoute(GetNavConfig? currentRoute) { - final String? currentLocation = currentRoute?.location; + final String? currentLocation = currentRoute?.uri.path; int currentIndex = 0; if (currentLocation != null) { - currentIndex = - tabs.indexWhere((tab) => currentLocation.startsWith(tab.path)); + currentIndex = tabs.indexWhere((tab) { + String parentPath = tab.parent?.path ?? ''; + String fullPath = '$parentPath${tab.path}'; + return currentLocation.startsWith(fullPath); + }); } return (currentIndex > 0) ? currentIndex : 0; } diff --git a/lib/app/utils/icon_constants.dart b/lib/app/utils/icon_constants.dart new file mode 100644 index 00000000..b3351a95 --- /dev/null +++ b/lib/app/utils/icon_constants.dart @@ -0,0 +1,5 @@ +abstract class IconConstants { + static const _assetsIcon = 'assets/icons'; + + static const logo = '$_assetsIcon/logo.png'; +} diff --git a/lib/app/utils/img_constants.dart b/lib/app/utils/img_constants.dart new file mode 100644 index 00000000..83a5c0c6 --- /dev/null +++ b/lib/app/utils/img_constants.dart @@ -0,0 +1,6 @@ +abstract class ImgConstants { + static const _assetsImg = 'assets/images'; + + static const dash = '$_assetsImg/dash.png'; + static const flutterfire = '$_assetsImg/flutterfire_300x.png'; +} diff --git a/lib/app/widgets/login_widgets.dart b/lib/app/widgets/login_widgets.dart index b8f2d8c1..63162b44 100644 --- a/lib/app/widgets/login_widgets.dart +++ b/lib/app/widgets/login_widgets.dart @@ -6,7 +6,9 @@ import 'package:get/get.dart'; import '../../services/auth_service.dart'; import '../../models/screens.dart'; import '../../services/remote_config.dart'; +import '../utils/img_constants.dart'; import 'menu_sheet_button.dart'; +import 'remotely_config_obx.dart'; class LoginWidgets { static Widget headerBuilder(context, constraints, shrinkOffset) { @@ -14,7 +16,7 @@ class LoginWidgets { padding: const EdgeInsets.all(20), child: AspectRatio( aspectRatio: 1, - child: Image.asset('assets/images/flutterfire_300x.png'), + child: Image.asset(ImgConstants.flutterfire), ), ); } @@ -38,7 +40,7 @@ class LoginWidgets { padding: const EdgeInsets.all(20), child: AspectRatio( aspectRatio: 1, - child: Image.asset('assets/images/flutterfire_300x.png'), + child: Image.asset(ImgConstants.flutterfire), ), ); } @@ -56,31 +58,36 @@ class LoginBottomSheetToggle extends MenuSheetButton { @override Icon? get icon => (AuthService.to.isLoggedInValue) - ? values.length == 1 + ? values.length <= 1 ? const Icon(Icons.logout) : const Icon(Icons.menu) : const Icon(Icons.login); @override String? get label => (AuthService.to.isLoggedInValue) - ? values.length == 1 + ? values.length <= 1 ? 'Logout' : 'Click for Options' : 'Login'; + @override + void buttonPressed(Iterable values) async => values.isEmpty + ? callbackFunc(await Screen.LOGOUT.doAction()) + : super.buttonPressed(values); + @override Widget build(BuildContext context) { - MenuItemsController controller = Get.put( - MenuItemsController([]), - permanent: true); //must make true else gives error - Screen.sheet(null).then((val) { - controller.values.value = val; - }); - RemoteConfig.instance.then((ins) => - ins.addUseBottomSheetForProfileOptionsListener((val) async => - {controller.values.value = await Screen.sheet(null)})); + MenuItemsController controller = + MenuItemsController(const Iterable.empty()); return Obx(() => (AuthService.to.isLoggedInValue) - ? builder(context, vals: controller.values.value) + ? RemotelyConfigObx( + () => builder(context, vals: controller.values.value), + controller, + Screen.sheet, + Screen.NONE, + "useBottomSheetForProfileOptions", + Typer.boolean, + ) : !(current.currentPage!.name == Screen.LOGIN.path) ? IconButton( onPressed: () async { diff --git a/lib/app/widgets/menu_sheet_button.dart b/lib/app/widgets/menu_sheet_button.dart index abd3873e..31649e4d 100644 --- a/lib/app/widgets/menu_sheet_button.dart +++ b/lib/app/widgets/menu_sheet_button.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../models/action_enum.dart'; +import 'remotely_config_obx.dart'; -class MenuItemsController extends GetxController { - MenuItemsController(Iterable iter) : values = Rx>(iter); - - final Rx> values; +class MenuItemsController + extends RemoteConfigController> { + MenuItemsController(super.iter); } class MenuSheetButton extends StatelessWidget { @@ -65,7 +65,7 @@ class MenuSheetButton extends StatelessWidget { //This should be a modal bottom sheet if on Mobile (See https://mercyjemosop.medium.com/select-and-upload-images-to-firebase-storage-flutter-6fac855970a9) Widget builder(BuildContext context, {Iterable? vals}) { Iterable values = vals ?? values_!; - return values.length == 1 || + return values.length <= 1 || Get.mediaQuery.orientation == Orientation.portrait // : Get.context!.isPortrait ? (icon != null diff --git a/lib/app/widgets/remotely_config_obx.dart b/lib/app/widgets/remotely_config_obx.dart new file mode 100644 index 00000000..684503c0 --- /dev/null +++ b/lib/app/widgets/remotely_config_obx.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../services/remote_config.dart'; + +class RemoteConfigController extends GetxController { + RemoteConfigController(L iter) : values = Rx(iter); + final Rx values; +} + +class RemotelyConfigObx>> + extends ObxWidget { + final Widget Function() builder; + final X param; + final C controller; + final String config; + final Typer configType; + final Future Function(X) func; + + RemotelyConfigObx(this.builder, this.controller, this.func, this.param, + this.config, this.configType, + {super.key}) { + Get.put(controller, permanent: true); //must make true else gives error + func(param).then((v) { + controller.values.value = v; + }); + RemoteConfig.instance.then((ins) => ins.addListener(config, configType, + (val) async => {controller.values.value = await func(param)})); + } + + @override + Widget build() => builder(); +} + +class RemotelyConfigObxVal extends ObxWidget { + final Widget Function(T) builder; + final T data; + final String config; + final Typer configType; + + RemotelyConfigObxVal(this.builder, this.data, this.config, this.configType, + {super.key, required Future Function(X) func, required X param}) { + func(param).then((v) => {data.value = v}); + RemoteConfig.instance.then((ins) => ins.addListener( + config, configType, (val) async => {data.value = await func(param)})); + } + + RemotelyConfigObxVal.noparam( + this.builder, this.data, this.config, this.configType, + {super.key, required Future Function() func}) { + func().then((v) => {data.value = v}); + RemoteConfig.instance.then((ins) => ins.addListener( + config, configType, (val) async => {data.value = await func()})); + } + + @override + Widget build() => builder(data); +} diff --git a/lib/app/widgets/screen_widget.dart b/lib/app/widgets/screen_widget.dart index d80c9275..afd335a5 100644 --- a/lib/app/widgets/screen_widget.dart +++ b/lib/app/widgets/screen_widget.dart @@ -3,15 +3,13 @@ import 'package:get/get.dart'; import '../routes/app_pages.dart'; import '../../models/role.dart'; import '../../models/screens.dart'; +import 'login_widgets.dart'; -class ScreenWidget extends StatelessWidget { +class ScreenWidget extends StatefulWidget { final Widget body; final Role? role; - final GetDelegate? delegate; - final GetNavConfig? currentRoute; - final Screen screen; final AppBar? appBar; @@ -25,33 +23,55 @@ class ScreenWidget extends StatelessWidget { this.appBar, }); + @override + _ScreenWidgetState createState() => _ScreenWidgetState(); +} + +class _ScreenWidgetState extends State { + late int _currentIndex; + + @override + void initState() { + super.initState(); + // Initialize currentIndex based on the role and route + _currentIndex = widget.role != null + ? widget.role!.getCurrentIndexFromRoute(widget.currentRoute) + : 0; + } + + void _onTabTapped(int index) { + setState(() { + _currentIndex = index; + }); + + if (widget.delegate != null) { + widget.role!.routeTo(index, widget.delegate!); + } + } + @override Widget build(BuildContext context) { - int currentIndex = - role != null ? role!.getCurrentIndexFromRoute(currentRoute) : 0; - Iterable fabs = screen.fabs; + bool isDesktop = GetPlatform.isDesktop; + Iterable fabs = widget.screen.fabs; + return Scaffold( - body: body, - appBar: appBar, - bottomNavigationBar: (screen.navTabs.isNotEmpty) - ? BottomNavigationBar( - currentIndex: currentIndex, - onTap: (value) { - if (delegate != null) { - role!.routeTo(value, delegate!); - } - }, - items: - role!.tabs //screen may have more navTabs but we need by role - .map((Screen tab) => BottomNavigationBarItem( - icon: Icon(tab.icon), - label: tab.label, - )) - .toList(), - ) - : null, + body: widget.body, + appBar: widget.appBar, + bottomNavigationBar: isDesktop || widget.screen.navTabs.isEmpty + ? null + : BottomNavigationBar( + currentIndex: _currentIndex, + selectedItemColor: Colors.black, + unselectedItemColor: Colors.grey, + onTap: _onTabTapped, + items: widget.role!.tabs + .map((Screen tab) => BottomNavigationBarItem( + icon: Icon(tab.icon), + label: tab.label, + )) + .toList(), + ), floatingActionButton: fabs.isNotEmpty ? getFAB(fabs) : null, - // bottomSheet: //this is used for persistent bar like status bar ); } @@ -73,3 +93,31 @@ class ScreenWidget extends StatelessWidget { return null; //TODO multi fab button on press } } + +extension ScreenWidgetExtension on Screen { + Widget? widget(GetNavConfig current) { + //those with accessor == widget must be handled here + switch (this) { + case Screen.SEARCH: + return IconButton(onPressed: () => {}, icon: Icon(icon)); + case Screen.LOGIN: + return LoginBottomSheetToggle(current); + case Screen.LOGOUT: + return LoginBottomSheetToggle(current); + default: + } + return null; + } + +//This could be used to add icon buttons in expanded web view instead of the context menu + static List topRightMenuButtons(GetNavConfig current) { + List widgets = []; + for (var screen in Screen.topRightMenu()) { + widgets.add(Container( + margin: const EdgeInsets.only(right: 15), + child: screen.widget(current))); + } + + return widgets; //This will return empty. We need a Obx + } +} diff --git a/lib/app/widgets/search_bar_button.dart b/lib/app/widgets/search_bar_button.dart new file mode 100644 index 00000000..14f60d37 --- /dev/null +++ b/lib/app/widgets/search_bar_button.dart @@ -0,0 +1,3 @@ +// This uses Remote Config to know where to locate the search button +// If on top, then it expands to title area on press in mobiles and is already expanded in web +// If on Nav Bar, the it initiates a SearchAnchor as a bottomsheet in mobile and like a drawer/nav column in web \ No newline at end of file diff --git a/lib/desktop_layout.dart b/lib/desktop_layout.dart new file mode 100644 index 00000000..ebc8662c --- /dev/null +++ b/lib/desktop_layout.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; + +class DesktopLayout extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Row( + children: [ + Container( + width: 250, + child: Column( + children: [ + DrawerHeader( + child: Text('Drawer Header'), + ), + ListTile( + title: Text('Screen 1'), + onTap: () {}, + ), + ListTile( + title: Text('Screen 2'), + onTap: () {}, + ), + ], + ), + ), + Expanded( + child: Column( + children: [ + AppBar( + title: Text('Desktop Layout'), + actions: [ + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 20.0), + child: TextField( + decoration: InputDecoration( + hintText: 'Search...', + border: OutlineInputBorder(), + suffixIcon: Icon(Icons.search), + ), + ), + ), + ), + PopupMenuButton( + onSelected: (item) => print('Selected $item'), + itemBuilder: (context) => [ + PopupMenuItem(value: 0, child: Text('Profile')), + PopupMenuItem(value: 1, child: Text('Settings')), + PopupMenuItem(value: 2, child: Text('Logout')), + ], + ), + ], + ), + Expanded( + child: Center( + child: Text('Desktop Body Content'), + ), + ), + Container( + height: 30, + color: Colors.grey.shade200, + child: Center( + child: Text('Status Bar - Desktop Only'), + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 30c258f2..047a9484 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,10 +4,15 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:g_recaptcha_v3/g_recaptcha_v3.dart'; import 'app/routes/app_pages.dart'; import 'firebase_options.dart'; import 'services/auth_service.dart'; +import 'services/persona_service.dart'; +import 'responsive_layout.dart'; +import 'mobile_layout.dart'; +import 'desktop_layout.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -15,7 +20,12 @@ void main() async { await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); - + if (GetPlatform.isWeb) { + final String siteKey = 'SITEKEY'; + bool ready = await GRecaptchaV3.ready(siteKey); + print("Is Recaptcha ready? $ready"); + } + Get.lazyPut(() => PersonaService()); runApp( GetMaterialApp.router( debugShowCheckedModeBanner: @@ -24,6 +34,7 @@ void main() async { initialBinding: BindingsBuilder( () { Get.put(AuthService()); + Get.put(PersonaService()); }, ), getPages: AppPages.routes, @@ -35,10 +46,26 @@ void main() async { // preventDuplicateHandlingMode: // PreventDuplicateHandlingMode.ReorderRoutes, // ), - theme: ThemeData( - highlightColor: Colors.black.withOpacity(0.5), - bottomSheetTheme: - const BottomSheetThemeData(surfaceTintColor: Colors.blue)), + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + themeMode: Get.find().themeMode, ), ); } + + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return GetMaterialApp( + title: 'Responsive App', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: ResponsiveLayout( + mobileLayout: MobileLayout(), + desktopLayout: DesktopLayout(), + ), + ); + } +} diff --git a/lib/mobile_layout.dart b/lib/mobile_layout.dart new file mode 100644 index 00000000..102c70fb --- /dev/null +++ b/lib/mobile_layout.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class MobileLayout extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Mobile Layout'), + actions: [ + IconButton( + icon: Icon(Icons.search), + onPressed: () { + // Implement search functionality + }, + ), + IconButton( + icon: Icon(Icons.account_circle), + onPressed: () { + Get.bottomSheet( + // Implement bottom sheet menu + Container( + child: Wrap( + children: [ + ListTile( + leading: Icon(Icons.person), + title: Text('Profile'), + onTap: () {}, + ), + ListTile( + leading: Icon(Icons.settings), + title: Text('Settings'), + onTap: () {}, + ), + ListTile( + leading: Icon(Icons.logout), + title: Text('Logout'), + onTap: () {}, + ), + ], + ), + ), + ); + }, + ), + ], + ), + drawer: Drawer( + child: ListView( + children: [ + DrawerHeader( + child: Text('Drawer Header'), + ), + ListTile( + title: Text('Screen 1'), + onTap: () {}, + ), + ListTile( + title: Text('Screen 2'), + onTap: () {}, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + // Implement FAB action + }, + child: Icon(Icons.add), + ), + body: Center( + child: Text('Mobile Body Content'), + ), + ); + } +} diff --git a/lib/models/access_level.dart b/lib/models/access_level.dart index a7b89742..20b79252 100644 --- a/lib/models/access_level.dart +++ b/lib/models/access_level.dart @@ -1,7 +1,7 @@ enum AccessLevel { + notAuthed, // used for login screens public, //available without any login guest, //available with guest login - notAuthed, // used for login screens authenticated, //available on login roleBased, //available on login and with allowed roles masked, //available in a partly masked manner based on role diff --git a/lib/models/product.dart b/lib/models/product.dart index 003d5785..03a791bc 100644 --- a/lib/models/product.dart +++ b/lib/models/product.dart @@ -1,9 +1,91 @@ class Product { - final String name; final String id; + final String name; + final String brandName; + final String category; + final String productImage; + final double price; Product({ - required this.name, required this.id, + required this.name, + required this.brandName, + required this.category, + required this.productImage, + required this.price, }); + + /// Factory constructor for creating a `Product` instance from a JSON map + factory Product.fromJson(Map json) { + return Product( + id: json['id'] as String, + name: json['name'] as String, + brandName: json['brandName'] as String, + category: json['category'] as String, + productImage: json['productImage'] as String, + price: + (json['price'] as num).toDouble(), // To ensure the price is a double + ); + } + + /// Method to convert a `Product` instance to a JSON map + Map toJson() { + return { + 'id': id, + 'name': name, + 'brandName': brandName, + 'category': category, + 'productImage': productImage, + 'price': price, + }; + } + + /// Method for creating a copy of the current `Product` instance with modified fields + Product copyWith({ + String? id, + String? name, + String? brandName, + String? category, + String? productImage, + double? price, + }) { + return Product( + id: id ?? this.id, + name: name ?? this.name, + brandName: brandName ?? this.brandName, + category: category ?? this.category, + productImage: productImage ?? this.productImage, + price: price ?? this.price, + ); + } + + /// Override `toString` for better debugging + @override + String toString() { + return 'Product{id: $id, name: $name, brandName: $brandName, category: $category, productImage: $productImage, price: $price}'; + } + + /// Override `==` and `hashCode` to compare products by their fields + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is Product && + other.id == id && + other.name == name && + other.brandName == brandName && + other.category == category && + other.productImage == productImage && + other.price == price; + } + + @override + int get hashCode { + return id.hashCode ^ + name.hashCode ^ + brandName.hashCode ^ + category.hashCode ^ + productImage.hashCode ^ + price.hashCode; + } } diff --git a/lib/models/screens.dart b/lib/models/screens.dart index 24dee39f..585f29f5 100644 --- a/lib/models/screens.dart +++ b/lib/models/screens.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import '../app/widgets/login_widgets.dart'; import '../services/remote_config.dart'; import 'action_enum.dart'; import 'access_level.dart'; @@ -8,9 +7,13 @@ import '../../services/auth_service.dart'; enum AccessedVia { auto, - widget, //example: top right button - navigator, //bottom nav. can be linked to drawer items //handled in ScreenWidget - drawer, //creates nav tree //handled in RootView + widget, + topRight, + topCenter, + topLeft, + topBar, //bar below the main top menu bar + navigator, //bottom nav. can be linked to drawer items. left strip in expanded web //handled in ScreenWidget + drawer, //creates nav tree. persistant in expanded web and linked with nav icons //handled in RootView bottomSheet, //context menu for web handled via the Button that calls the sheet fab, //handled in ScreenWidget singleTap, //when an item of a list is clicked @@ -18,6 +21,7 @@ enum AccessedVia { } enum Screen implements ActionEnum { + NONE.none(), //null HOME('/home', icon: Icons.home, label: "Home", @@ -37,22 +41,18 @@ enum Screen implements ActionEnum { parent: HOME), PRODUCT_DETAILS('/:productId', accessLevel: AccessLevel.public, parent: PRODUCTS), - LOGIN('/login', - icon: Icons.login, - accessor_: AccessedVia.widget, - accessLevel: AccessLevel.notAuthed), PROFILE('/profile', icon: Icons.account_box_rounded, label: "Profile", accessor_: AccessedVia.drawer, accessLevel: AccessLevel.authenticated, - remoteConfig: true), + remoteConfig: "useBottomSheetForProfileOptions"), SETTINGS('/settings', icon: Icons.settings, label: "Settings", accessor_: AccessedVia.drawer, accessLevel: AccessLevel.authenticated, - remoteConfig: true), + remoteConfig: "useBottomSheetForProfileOptions"), CART('/cart', icon: Icons.trolley, label: "Cart", @@ -96,20 +96,40 @@ enum Screen implements ActionEnum { accessLevel: AccessLevel.roleBased), MY_PRODUCT_DETAILS('/:productId', parent: MY_PRODUCTS, accessLevel: AccessLevel.roleBased), + SEARCH('/search', + icon: Icons.search, + label: "Search", + accessor_: AccessedVia.topRight, + remoteConfig: "showSearchBarOnTop", + accessLevel: AccessLevel.public), LOGOUT('/login', icon: Icons.logout, label: "Logout", - accessor_: AccessedVia.bottomSheet, + accessor_: AccessedVia.topRight, + remoteConfig: "useBottomSheetForProfileOptions", accessLevel: AccessLevel.authenticated), + LOGIN('/login', + icon: Icons.login, + accessor_: AccessedVia.topRight, + accessLevel: AccessLevel.notAuthed), ; const Screen(this.path, {this.icon, this.label, - this.parent, + this.parent = Screen.NONE, this.accessor_ = AccessedVia.singleTap, this.accessLevel = AccessLevel.authenticated, - this.remoteConfig = false}); + this.remoteConfig}); + + const Screen.none() + : path = '', + icon = null, + label = null, + parent = null, + accessor_ = AccessedVia.singleTap, + accessLevel = AccessLevel.authenticated, + remoteConfig = null; @override final IconData? icon; @@ -121,10 +141,10 @@ enum Screen implements ActionEnum { final Screen? parent; final AccessLevel accessLevel; //if false it is role based. true means allowed for all - final bool remoteConfig; + final String? remoteConfig; Future get accessor async { - if (remoteConfig && + if (remoteConfig == "useBottomSheetForProfileOptions" && (await RemoteConfig.instance).useBottomSheetForProfileOptions()) { return AccessedVia.bottomSheet; } @@ -164,6 +184,12 @@ enum Screen implements ActionEnum { return list; } + static Iterable topRightMenu() { + return Screen.values.where((Screen screen) => + screen.accessor_ == AccessedVia.topRight && + AuthService.to.accessLevel.index >= screen.accessLevel.index); + } + @override Future doAction() async { if (this == LOGOUT) { @@ -171,7 +197,4 @@ enum Screen implements ActionEnum { } Get.rootDelegate.toNamed(route); } - - Widget? widget(GetNavConfig current) => - (this == LOGIN) ? LoginBottomSheetToggle(current) : null; } diff --git a/lib/responsive_layout.dart b/lib/responsive_layout.dart new file mode 100644 index 00000000..3105fa26 --- /dev/null +++ b/lib/responsive_layout.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +class ResponsiveLayout extends StatelessWidget { + final Widget mobileLayout; + final Widget desktopLayout; + + const ResponsiveLayout({ + required this.mobileLayout, + required this.desktopLayout, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < 600) { + return mobileLayout; + } else { + return desktopLayout; + } + }, + ); + } +} diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 8bf72aaa..f33933b5 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -1,14 +1,14 @@ -// ignore_for_file: avoid_print - import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_ui_auth/firebase_ui_auth.dart' as fbui; import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:get_flutter_fire/models/access_level.dart'; import '../models/screens.dart'; import '../constants.dart'; import '../models/role.dart'; +import 'persona_service.dart'; class AuthService extends GetxService { static AuthService get to => Get.find(); @@ -32,11 +32,21 @@ class AuthService extends GetxService { if (user != null) { user.getIdTokenResult().then((token) { _userRole.value = Role.fromString(token.claims?["role"]); + final personaService = Get.find(); + personaService.loadSelectedPersona(); }); } }); } + AccessLevel get accessLevel => user != null + ? user!.isAnonymous + ? _userRole.value.index > Role.buyer.index + ? AccessLevel.roleBased + : AccessLevel.authenticated + : AccessLevel.guest + : AccessLevel.public; + bool get isEmailVerified => user != null && (user!.email == null || user!.emailVerified); @@ -52,6 +62,9 @@ class AuthService extends GetxService { ? (user!.displayName ?? user!.email) : 'Guest'; + String? get userPhotoUrl => user?.photoURL; + String? get userEmail => user?.email; + void login() { // this is not needed as we are using Firebase UI for the login part } @@ -109,13 +122,59 @@ class AuthService extends GetxService { void register() { registered.value = true; - // logout(); // Uncomment if we need to enforce relogin final thenTo = Get.rootDelegate.currentConfiguration!.currentPage!.parameters?['then']; Get.rootDelegate .offAndToNamed(thenTo ?? Screen.PROFILE.route); //Profile has the forms } + Future signInAnonymously() async { + try { + final userCredential = await _auth.signInAnonymously(); + print("Signed in with temporary account."); + _firebaseUser.value = userCredential.user; + } on FirebaseAuthException catch (e) { + switch (e.code) { + case "operation-not-allowed": + print("Anonymous auth hasn't been enabled for this project."); + break; + default: + print("Unknown error: ${e.message}"); + } + } + } + + Future linkAnonymousAccountWithCredential( + AuthCredential credential) async { + try { + final userCredential = + await _auth.currentUser?.linkWithCredential(credential); + print("Anonymous account successfully upgraded"); + _firebaseUser.value = userCredential?.user; + } on FirebaseAuthException catch (e) { + switch (e.code) { + case "provider-already-linked": + print("The provider has already been linked to the user."); + break; + case "invalid-credential": + print("The provider's credential is not valid."); + break; + case "credential-already-in-use": + print( + "The account corresponding to the credential already exists, or is already linked to a Firebase User."); + break; + default: + print("Unknown error: ${e.message}"); + } + } + } + + Future convertAnonymousAccount(String email, String password) async { + final credential = + EmailAuthProvider.credential(email: email, password: password); + await linkAnonymousAccountWithCredential(credential); + } + void logout() { _auth.signOut(); if (isAnon) _auth.currentUser?.delete(); @@ -181,6 +240,25 @@ class AuthService extends GetxService { }; }; } + + Future sendPasswordResetEmail(String email) async { + try { + await _auth.sendPasswordResetEmail(email: email); + Get.snackbar( + 'Password Reset', + 'A password reset link has been sent to your email.', + snackPosition: SnackPosition.BOTTOM, + duration: Duration(seconds: 3), + ); + } on FirebaseAuthException catch (e) { + Get.snackbar( + 'Error', + 'Failed to send password reset email: ${e.message}', + snackPosition: SnackPosition.BOTTOM, + duration: Duration(seconds: 3), + ); + } + } } class MyCredential extends AuthCredential { diff --git a/lib/services/persona_service.dart b/lib/services/persona_service.dart new file mode 100644 index 00000000..eee347d5 --- /dev/null +++ b/lib/services/persona_service.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_storage/get_storage.dart'; + +class Persona { + final String name; + final ThemeData themeData; + final ThemeData darkThemeData; + final String imageUrl; + final Color titleColor; + final Color textColor; + final Color drawerColor; + final Color drawerColor1; + + Persona({ + required this.name, + required this.themeData, + required this.darkThemeData, + required this.titleColor, + required this.textColor, + required this.drawerColor, + required this.drawerColor1, + this.imageUrl = '', + }); +} + +class PersonaService extends GetxService { + final GetStorage _storage = GetStorage(); + final Rx _selectedPersona = Persona( + name: 'Default', + themeData: ThemeData.light().copyWith(scaffoldBackgroundColor: Colors.white), + darkThemeData: ThemeData.dark(), + imageUrl: '', + titleColor: Colors.black, + textColor: Colors.black, + drawerColor: const Color.fromARGB(255, 255, 255, 255), + drawerColor1: const Color.fromARGB(255, 211, 211, 211), + ).obs; + final Rx _themeMode = ThemeMode.light.obs; + + final personas = [ + Persona( + name: 'Default', + themeData: ThemeData.light().copyWith(scaffoldBackgroundColor: Colors.white), + darkThemeData: ThemeData.dark(), + imageUrl: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSmEedEwy284CMHPcPoYXz8i9K1BkGpLyHRUo2IleoVqnEa9cVD3pgtZdu0AHVQUnTDqKY&usqp=CAUR', + titleColor: Colors.black, + textColor: Colors.black, + drawerColor: const Color.fromARGB(255, 255, 255, 255), + drawerColor1: const Color.fromARGB(255, 211, 211, 211), + ), + Persona( + name: 'Kids', + themeData: ThemeData( + primarySwatch: Colors.pink, + scaffoldBackgroundColor: Color.fromARGB(255, 244, 157, 186), + ), + darkThemeData: ThemeData( + primarySwatch: Colors.green, + scaffoldBackgroundColor: Color.fromARGB(255, 166, 127, 168), + brightness: Brightness.dark, + ), + titleColor: Colors.black, + textColor: Colors.black, + drawerColor: const Color.fromARGB(255, 255, 255, 255), + drawerColor1: const Color.fromARGB(255, 255, 255, 255), + imageUrl: 'https://static.vecteezy.com/system/resources/previews/035/867/277/original/ai-generated-cute-little-boy-with-smile-icon-illustration-avatar-of-cute-handsome-boy-cartoon-style-vector.jpg', + ), + Persona( + name: 'Tech Savvy', + themeData: ThemeData( + primarySwatch: Colors.blueGrey, + scaffoldBackgroundColor: Colors.blueGrey[50], + ), + darkThemeData: ThemeData( + primarySwatch: Colors.blueGrey, + scaffoldBackgroundColor: Colors.blueGrey[900], + brightness: Brightness.dark, + ), + titleColor: Colors.black, + textColor: Colors.black, + drawerColor: const Color.fromARGB(255, 255, 255, 255), + drawerColor1: const Color.fromARGB(255, 255, 255, 255), + imageUrl: 'https://img.freepik.com/premium-photo/neon-character-tech-savvy-chibi-boy-with-undercut-hairstyle-hacker-hoodie-clipart-sticker-set_655090-1193425.jpg?w=740', + ), + Persona( + name: 'Nature Lover', + themeData: ThemeData( + primarySwatch: Colors.green, + scaffoldBackgroundColor: Colors.lightGreen[50], + ), + darkThemeData: ThemeData( + primarySwatch: Colors.green, + scaffoldBackgroundColor: Colors.lightGreen[900], + brightness: Brightness.dark, + ), + titleColor: Colors.black, + textColor: Colors.black, + drawerColor: const Color.fromARGB(255, 255, 255, 255), + drawerColor1: const Color.fromARGB(255, 255, 255, 255), + imageUrl: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRxPkSUSZlKiUTK94du4ILsKpvFMrKi32VWLOsngdc3YSIeACsE58AchtZewsNkIfM6Ge0&usqp=CAU', + ), + ]; + + Persona get selectedPersona => _selectedPersona.value; + + ThemeMode get themeMode => _themeMode.value; + + @override + void onInit() { + super.onInit(); + loadSelectedPersona(); + } + + void loadSelectedPersona() { + final name = _storage.read('selectedPersona') ?? 'Default'; + final isDarkMode = _storage.read('isDarkMode') ?? false; + _selectedPersona.value = personas.firstWhere((persona) => persona.name == name); + _themeMode.value = isDarkMode ? ThemeMode.dark : ThemeMode.light; + Get.changeThemeMode(_themeMode.value); + Get.changeTheme(isDarkMode ? selectedPersona.darkThemeData : selectedPersona.themeData); + } + + void selectPersona(Persona persona) { + _selectedPersona.value = persona; + _storage.write('selectedPersona', persona.name); + final isDarkMode = _storage.read('isDarkMode') ?? false; + _themeMode.value = isDarkMode ? ThemeMode.dark : ThemeMode.light; + // Get.changeThemeMode(_themeMode.value); + Get.changeTheme(isDarkMode ? persona.darkThemeData : persona.themeData); + } + + void toggleDarkMode(bool isDarkMode) { + _storage.write('isDarkMode', isDarkMode); + _themeMode.value = isDarkMode ? ThemeMode.dark : ThemeMode.light; + // Get.changeThemeMode(_themeMode.value); + Get.changeTheme(isDarkMode ? selectedPersona.darkThemeData : selectedPersona.themeData); + } +} diff --git a/lib/services/remote_config.dart b/lib/services/remote_config.dart index 5d1145a5..cb2a8928 100644 --- a/lib/services/remote_config.dart +++ b/lib/services/remote_config.dart @@ -13,7 +13,6 @@ class RemoteConfig { } final FirebaseRemoteConfig _remoteConfig = FirebaseRemoteConfig.instance; - final List listeners = []; Future init() async { await _remoteConfig.setConfigSettings(RemoteConfigSettings( @@ -68,11 +67,4 @@ class RemoteConfig { bool showSearchBarOnTop() { return _remoteConfig.getBool("showSearchBarOnTop"); } - - void addUseBottomSheetForProfileOptionsListener(listener) { - addListener("useBottomSheetForProfileOptions", Typer.boolean, listener); - if (!listeners.contains(listener)) { - listeners.add(listener); - } - } } diff --git a/lib/utils/size_config.dart b/lib/utils/size_config.dart new file mode 100644 index 00000000..9470d795 --- /dev/null +++ b/lib/utils/size_config.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart'; + +class SizeConfig { + static late double h; + static late double w; + + static void init(BuildContext context) { + h = MediaQuery.of(context).size.height; + w = MediaQuery.of(context).size.width; + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 877fc75e..1f376379 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.35" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" args: dependency: transitive description: @@ -33,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + carousel_slider: + dependency: "direct main" + description: + name: carousel_slider + sha256: "7b006ec356205054af5beaef62e2221160ea36b90fb70a35e4deacd49d0349ae" + url: "https://pub.dev" + source: hosted + version: "5.0.0" characters: dependency: transitive description: @@ -61,18 +77,18 @@ packages: dependency: transitive description: name: cross_file - sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.4+1" + version: "0.3.4+2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" cupertino_icons: dependency: "direct main" description: @@ -109,18 +125,18 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file_picker: dependency: "direct main" description: name: file_picker - sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a" + sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3" url: "https://pub.dev" source: hosted - version: "8.0.3" + version: "8.0.7" file_selector_linux: dependency: transitive description: @@ -149,10 +165,10 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.3+2" firebase_analytics: dependency: "direct main" description: @@ -181,26 +197,26 @@ packages: dependency: "direct main" description: name: firebase_auth - sha256: f0a75f61992d036e4c46ad0e9febd364d98aa2c092690a5475cb1421a8243cfe + sha256: cfc2d970829202eca09e2896f0a5aa7c87302817ecc0bdfa954f026046bf10ba url: "https://pub.dev" source: hosted - version: "4.19.5" + version: "4.20.0" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: feb77258404309ffc7761c78e1c0ad2ed5e4fdc378e035619e2cc13be4397b62 + sha256: a0270e1db3b2098a14cb2a2342b3cd2e7e458e0c391b1f64f6f78b14296ec093 url: "https://pub.dev" source: hosted - version: "7.2.6" + version: "7.3.0" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: "6d527f357da2bf93a67a42b423aa92943104a0c290d1d72ad9a42c779d501cd2" + sha256: "64e067e763c6378b7e774e872f0f59f6812885e43020e25cde08f42e9459837b" url: "https://pub.dev" source: hosted - version: "5.11.5" + version: "5.12.0" firebase_core: dependency: "direct main" description: @@ -213,34 +229,34 @@ packages: dependency: transitive description: name: firebase_core_platform_interface - sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63 + sha256: "3c3a1e92d6f4916c32deea79c4a7587aa0e9dbbe5889c7a16afcf005a485ee02" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.2.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "43d9e951ac52b87ae9cc38ecdcca1e8fa7b52a1dd26a96085ba41ce5108db8e9" + sha256: e8d1e22de72cb21cdcfc5eed7acddab3e99cd83f3b317f54f7a96c32f25fd11e url: "https://pub.dev" source: hosted - version: "2.17.0" + version: "2.17.4" firebase_dynamic_links: dependency: transitive description: name: firebase_dynamic_links - sha256: f704859abc17d99e74b47eaf47455b45a88ab7e2973f03e6130ff666b45fe11f + sha256: "47b8c8a8546d8a7f9000edb90848549f20b137d814ee7e0407b3d43b8445e282" url: "https://pub.dev" source: hosted - version: "5.5.5" + version: "5.5.7" firebase_dynamic_links_platform_interface: dependency: transitive description: name: firebase_dynamic_links_platform_interface - sha256: f86992605b50e2f0ce6c24993430affc98021da8d8a74d5596b7a2c84196c110 + sha256: "72e7810635f908ce060c5803c7acb29116c5b6befc73e90446c52722bc9506a2" url: "https://pub.dev" source: hosted - version: "0.2.6+33" + version: "0.2.6+35" firebase_remote_config: dependency: "direct main" description: @@ -269,26 +285,26 @@ packages: dependency: "direct main" description: name: firebase_storage - sha256: da76ca9c11d795c4bae1bd13b31d54bb9eb9ccbee7eb5f6b86b8294370e9d488 + sha256: "2ae478ceec9f458c1bcbf0ee3e0100e4e909708979e83f16d5d9fba35a5b42c1" url: "https://pub.dev" source: hosted - version: "11.7.5" + version: "11.7.7" firebase_storage_platform_interface: dependency: transitive description: name: firebase_storage_platform_interface - sha256: be17bfa9110a6429b40dd3760c755034079fd734aa1dd2476d5638ab780cc508 + sha256: "4e18662e6a66e2e0e181c06f94707de06d5097d70cfe2b5141bf64660c5b5da9" url: "https://pub.dev" source: hosted - version: "5.1.20" + version: "5.1.22" firebase_storage_web: dependency: transitive description: name: firebase_storage_web - sha256: "5219c20c0768a8e2ffedf0a116b7bc80ab32fcc6e2cbd50cbde14f8c4575c3f4" + sha256: "3a44aacd38a372efb159f6fe36bb4a7d79823949383816457fd43d3d47602a53" url: "https://pub.dev" source: hosted - version: "3.9.5" + version: "3.9.7" firebase_ui_auth: dependency: "direct main" description: @@ -351,10 +367,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "8cf40eebf5dec866a6d1956ad7b4f7016e6c0cc69847ab946833b7d43743809f" + sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de" url: "https://pub.dev" source: hosted - version: "2.0.19" + version: "2.0.21" flutter_svg: dependency: transitive description: @@ -373,6 +389,14 @@ packages: description: flutter source: sdk version: "0.0.0" + g_recaptcha_v3: + dependency: "direct main" + description: + name: g_recaptcha_v3 + sha256: b493d9bbad64bb4631a2b7bb86f7b4c6c6a3c2b327729c4130b3c95817bedf29 + url: "https://pub.dev" + source: hosted + version: "0.0.6" get: dependency: "direct main" description: @@ -401,10 +425,10 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "9482364c9f8b7bd36902572ebc3a7c2b5c8ee57a9c93e6eb5099c1a9ec5265d8" + sha256: "5be191523702ba8d7a01ca97c17fca096822ccf246b0a9f11923a6ded06199b6" url: "https://pub.dev" source: hosted - version: "0.3.1+1" + version: "0.3.1+4" google_sign_in: dependency: "direct main" description: @@ -417,10 +441,10 @@ packages: dependency: transitive description: name: google_sign_in_android - sha256: "7647893c65e6720973f0e579051c8f84b877b486614d9f70a404259c41a4632e" + sha256: "5a47ebec9af97daf0822e800e4f101c3340b5ebc3f6898cf860c1a71b53cf077" url: "https://pub.dev" source: hosted - version: "6.1.23" + version: "6.1.28" google_sign_in_ios: dependency: transitive description: @@ -441,18 +465,18 @@ packages: dependency: transitive description: name: google_sign_in_web - sha256: fc0f14ed45ea616a6cfb4d1c7534c2221b7092cc4f29a709f0c3053cc3e821bd + sha256: "042805a21127a85b0dc46bba98a37926f17d2439720e8a459d27045d8ef68055" url: "https://pub.dev" source: hosted - version: "0.12.4" + version: "0.12.4+2" http: dependency: transitive description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http_parser: dependency: transitive description: @@ -465,34 +489,34 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "33974eca2e87e8b4e3727f1b94fa3abcb25afe80b6bc2c4d449a0e150aedf720" + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "79455f6cff4cbef583b2b524bbf0d4ec424e5959f4d464e36ef5323715b98370" + sha256: "8c5abf0dcc24fe6e8e0b4a5c0b51a5cf30cefdf6407a3213dae61edc75a70f56" url: "https://pub.dev" source: hosted - version: "0.8.12" + version: "0.8.12+12" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3" + sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.5" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: cb0db0ec0d3e2cd49674f2e6053be25ccdb959832607c1cbd215dd6cf10fb0dd + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" url: "https://pub.dev" source: hosted - version: "0.8.11" + version: "0.8.12" image_picker_linux: dependency: transitive description: @@ -533,22 +557,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -565,6 +597,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + logger: + dependency: "direct main" + description: + name: logger + sha256: "7ad7215c15420a102ec687bb320a7312afd449bac63bfb1c60d9787c27b9767f" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + lottie: + dependency: "direct main" + description: + name: lottie + sha256: a93542cc2d60a7057255405f62252533f8e8956e7e06754955669fd32fb4b216 + url: "https://pub.dev" + source: hosted + version: "2.7.0" matcher: dependency: transitive description: @@ -577,18 +625,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -617,18 +665,18 @@ packages: dependency: transitive description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.10" path_provider_foundation: dependency: transitive description: @@ -657,10 +705,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: @@ -673,10 +721,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -734,10 +782,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" typed_data: dependency: transitive description: @@ -782,10 +830,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.4" web: dependency: transitive description: @@ -798,10 +846,10 @@ packages: dependency: transitive description: name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.5.4" xdg_directories: dependency: transitive description: @@ -819,5 +867,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.3.4 <4.0.0" - flutter: ">=3.19.2" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 2909a374..7921f36a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,13 +6,15 @@ environment: sdk: '>=3.3.4 <4.0.0' dependencies: + logger: ^1.1.0 + g_recaptcha_v3: 0.0.6 cupertino_icons: ^1.0.6 get: 4.6.6 flutter: sdk: flutter - firebase_core: ^2.31.0 + firebase_core: ^2.32.0 firebase_ui_auth: ^1.14.0 - firebase_auth: ^4.19.5 + firebase_auth: ^4.20.0 google_sign_in: ^6.2.1 firebase_ui_oauth_google: ^1.3.2 google_fonts: ^6.2.1 @@ -24,6 +26,8 @@ dependencies: firebase_ui_localizations: ^1.12.0 firebase_remote_config: ^4.4.7 firebase_analytics: ^10.10.7 + carousel_slider: ^5.0.0 + lottie: ^2.0.0 dev_dependencies: flutter_lints: 3.0.2 diff --git a/server/firestore.rules b/server/firestore.rules index 48798134..e3432c21 100644 --- a/server/firestore.rules +++ b/server/firestore.rules @@ -1,16 +1,9 @@ -service cloud.firestore { - match /databases/{database}/documents { - match /{document=**} { - // This rule allows anyone with your database reference to view, edit, - // and delete all data in your database. It is useful for getting - // started, but it is configured to expire after 30 days because it - // leaves your app open to attackers. At that time, all client - // requests to your database will be denied. - // - // Make sure to write security rules for your app before that time, or - // else all client requests to your database will be denied until you - // update your rules. - allow read, write: if request.time < timestamp.date(2024, 7, 3); +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow write: if request.auth.uid != null; + allow read: if true; } } } +s \ No newline at end of file