diff --git a/CHANGELOG.md b/CHANGELOG.md index ef1067e..c508264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 3.0.0 +- Refactored the project structure +- Added `flutter_shopping_interface` package +- Implemented default design + ## 2.0.0 - Added `flutter_shopping_interface` package - Implemented default design diff --git a/packages/flutter_shopping_cart/.gitignore b/packages/firebase_shopping_repository/.gitignore similarity index 52% rename from packages/flutter_shopping_cart/.gitignore rename to packages/firebase_shopping_repository/.gitignore index e31020f..ac5aa98 100644 --- a/packages/flutter_shopping_cart/.gitignore +++ b/packages/firebase_shopping_repository/.gitignore @@ -9,7 +9,6 @@ .history .svn/ migrate_working_dir/ -.metadata # IntelliJ related *.iml @@ -20,30 +19,11 @@ migrate_working_dir/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -.vscode/ +#.vscode/ # Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock **/doc/api/ -**/ios/Flutter/.last_build_id .dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ -pubspec.lock - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release - -# env -*dotenv +build/ diff --git a/packages/firebase_shopping_repository/lib/firebase_shopping_repository.dart b/packages/firebase_shopping_repository/lib/firebase_shopping_repository.dart new file mode 100644 index 0000000..fdf9281 --- /dev/null +++ b/packages/firebase_shopping_repository/lib/firebase_shopping_repository.dart @@ -0,0 +1,6 @@ +library firebase_shopping_repository; + +export 'src/firebase_category_repository.dart'; +export 'src/firebase_product_repository.dart'; +export 'src/firebase_shop_repository.dart'; +export 'src/firebase_shopping_cart_repository.dart'; diff --git a/packages/firebase_shopping_repository/lib/src/firebase_category_repository.dart b/packages/firebase_shopping_repository/lib/src/firebase_category_repository.dart new file mode 100644 index 0000000..5d2d440 --- /dev/null +++ b/packages/firebase_shopping_repository/lib/src/firebase_category_repository.dart @@ -0,0 +1,53 @@ +import 'dart:async'; + +import 'package:rxdart/rxdart.dart'; +import 'package:shopping_repository_interface/shopping_repository_interface.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +class FirebaseCategoryRepository implements CategoryRepositoryInterface { + final StreamController> _categoryController = + BehaviorSubject>(); + + final StreamController> _selectedCategoriesController = + BehaviorSubject>(); + + List _selectedCategories = []; + + List _categories = []; + @override + void deselectCategory(String? categoryId) { + _selectedCategories.removeWhere((category) => category.id == categoryId); + _selectedCategoriesController.add(_selectedCategories); + } + + @override + Stream> getCategories() { + FirebaseFirestore.instance + .collection('shopping_category') + .snapshots() + .listen((event) { + List categories = []; + event.docs.forEach((element) { + categories.add(Category.fromMap(element.id, element.data())); + }); + _categoryController.add(categories); + _categories = categories; + }); + + return _categoryController.stream; + } + + @override + Stream?> getSelectedCategoryStream() { + _selectedCategoriesController.add(_selectedCategories); + return _selectedCategoriesController.stream; + } + + @override + Category? selectCategory(String? categoryId) { + _selectedCategories + .add(_categories.firstWhere((category) => category.id == categoryId)); + _selectedCategoriesController.add(_selectedCategories); + return _selectedCategories.last; + } +} diff --git a/packages/firebase_shopping_repository/lib/src/firebase_product_repository.dart b/packages/firebase_shopping_repository/lib/src/firebase_product_repository.dart new file mode 100644 index 0000000..4746092 --- /dev/null +++ b/packages/firebase_shopping_repository/lib/src/firebase_product_repository.dart @@ -0,0 +1,60 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:collection/collection.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:shopping_repository_interface/shopping_repository_interface.dart'; + +class FirebaseProductRepository implements ProductRepositoryInterface { + /// Shop one product + + final List _products = []; + + Product? _selectedProduct; + + final StreamController> _productStream = + BehaviorSubject>(); + + @override + Product? getProduct(String? productId) => + _products.firstWhere((product) => product.id == productId); + + @override + Stream> getProducts(List? categories, String shopId) { + FirebaseFirestore.instance + .collection('shopping_products') + .doc(shopId) + .snapshots() + .listen((event) { + _products.clear(); + + if (event.data() == null) return; + var shopProducts = event.data()!['products'] as List; + print(categories); + if (categories != null && categories.isNotEmpty) + shopProducts = shopProducts + .where( + (product) => categories + .any((category) => category.name == product['category']), + ) + .toList(); + shopProducts.forEach((product) { + _products.add(Product.fromMap(product)); + }); + _productStream.add(_products); + }); + + return _productStream.stream; + } + + @override + Product? selectProduct(String? productId) { + _selectedProduct = + _products.firstWhere((product) => product.id == productId); + return _selectedProduct; + } + + @override + Product? getWeeklyOffer() => + _products.firstWhereOrNull((product) => product.isDiscounted); +} diff --git a/packages/firebase_shopping_repository/lib/src/firebase_shop_repository.dart b/packages/firebase_shopping_repository/lib/src/firebase_shop_repository.dart new file mode 100644 index 0000000..7478ea0 --- /dev/null +++ b/packages/firebase_shopping_repository/lib/src/firebase_shop_repository.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:rxdart/rxdart.dart'; +import 'package:shopping_repository_interface/shopping_repository_interface.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +class FirebaseShopRepository implements ShopRepositoryInterface { + final StreamController> _shopController = + BehaviorSubject>(); + + Shop? _selectedShop; + + List _shops = []; + + @override + Shop? getSelectedShop() { + return _selectedShop; + } + + @override + Shop? getShop(String? shopId) { + return _shops.firstWhere((element) => element.id == shopId); + } + + @override + Stream> getShops() { + FirebaseFirestore.instance + .collection('shopping_shop') + .snapshots() + .listen((event) { + List shops = []; + event.docs.forEach((element) { + shops.add(Shop.fromMap(element.id, element.data())); + }); + _shops = shops; + _shopController.add(shops); + }); + + return _shopController.stream; + } + + @override + Shop? selectShop(String? shopId) { + _selectedShop = _shops.firstWhere((element) => element.id == shopId); + return _selectedShop; + } +} diff --git a/packages/firebase_shopping_repository/lib/src/firebase_shopping_cart_repository.dart b/packages/firebase_shopping_repository/lib/src/firebase_shopping_cart_repository.dart new file mode 100644 index 0000000..864fdf7 --- /dev/null +++ b/packages/firebase_shopping_repository/lib/src/firebase_shopping_cart_repository.dart @@ -0,0 +1,78 @@ +import 'dart:async'; + +import 'package:rxdart/rxdart.dart'; +import 'package:shopping_repository_interface/shopping_repository_interface.dart'; + +class FirebaseShoppingCartRepository + implements ShoppingCartRepositoryInterface { + var _cart = ShoppingCart(id: "1", products: []); + + final StreamController _shoppingCartController = + BehaviorSubject(); + + @override + Future addProductToCart(Product product) async { + var existingProducts = _cart.products; + var index = existingProducts.indexWhere((p) => p.id == product.id); + + if (index != -1) { + existingProducts[index] = product.copyWith( + selectedAmount: existingProducts[index].selectedAmount + 1, + ); + _cart = _cart.copyWith( + products: existingProducts, + totalAmount: _cart.totalAmount + product.price, + totalAmountWithDiscount: product.isDiscounted + ? _cart.totalAmountWithDiscount + product.discountPrice + : _cart.totalAmountWithDiscount + product.price, + ); + } else { + _cart = _cart.copyWith( + products: [...existingProducts, product.copyWith(selectedAmount: 1)], + totalAmount: _cart.totalAmount + product.price, + totalAmountWithDiscount: product.isDiscounted + ? _cart.totalAmountWithDiscount + product.discountPrice + : _cart.totalAmountWithDiscount + product.price, + ); + } + + _shoppingCartController.add(_cart); + } + + @override + Stream getCartLength() { + return _shoppingCartController.stream.map((cart) => cart.products.length); + } + + @override + Stream getShoppingCart() { + return _shoppingCartController.stream; + } + + @override + Future removeProductFromCart(Product product) { + var existingProducts = _cart.products; + if (existingProducts.contains(product)) { + var index = existingProducts.indexOf(product); + if (product.selectedAmount == 1) { + existingProducts.removeAt(index); + } else { + existingProducts[index] = product.copyWith( + selectedAmount: existingProducts[index].selectedAmount - 1, + ); + } + + _cart = _cart.copyWith( + products: existingProducts, + totalAmount: _cart.totalAmount - product.price, + totalAmountWithDiscount: product.isDiscounted + ? _cart.totalAmountWithDiscount - product.discountPrice + : _cart.totalAmountWithDiscount - product.price, + ); + + _shoppingCartController.add(_cart); + } + + return Future.value(); + } +} diff --git a/packages/firebase_shopping_repository/pubspec.yaml b/packages/firebase_shopping_repository/pubspec.yaml new file mode 100644 index 0000000..623551b --- /dev/null +++ b/packages/firebase_shopping_repository/pubspec.yaml @@ -0,0 +1,25 @@ +name: firebase_shopping_repository +description: "A new Flutter package project." +version: 3.0.0 +homepage: https://github.com/Iconica-Development/flutter_shopping/packages/firebase_shopping_repository +publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ + +environment: + sdk: ^3.5.3 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + collection: ^1.18.0 + rxdart: ^0.28.0 + firebase_core: ^3.6.0 + cloud_firestore: ^5.4.4 + + shopping_repository_interface: + hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ + version: ^3.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/packages/flutter_order_details/README.md b/packages/flutter_order_details/README.md deleted file mode 100644 index b736730..0000000 --- a/packages/flutter_order_details/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# flutter_order_details - -This component contains TODO... - -## Features - -* TODO... - -## Usage - -First, TODO... - -For a more detailed example you can see the [example](https://github.com/Iconica-Development/flutter_order_details/tree/main/example). - -Or, you could run the example yourself: -``` -git clone https://github.com/Iconica-Development/flutter_order_details.git - -cd flutter_order_details - -cd example - -flutter run -``` - -## Issues - -Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_order_details) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl). - -## Want to contribute - -If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_order_details/pulls). - -## Author - -This flutter_order_details for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at diff --git a/packages/flutter_order_details/lib/flutter_order_details.dart b/packages/flutter_order_details/lib/flutter_order_details.dart deleted file mode 100644 index d22fb55..0000000 --- a/packages/flutter_order_details/lib/flutter_order_details.dart +++ /dev/null @@ -1,9 +0,0 @@ -/// Flutter component for shopping cart. -library flutter_order_details; - -export "package:flutter_form_wizard/flutter_form.dart"; - -export "src/configuration/order_detail_configuration.dart"; -export "src/configuration/order_detail_translations.dart"; -export "src/order_detail_screen.dart"; -export "src/widgets/order_succes.dart"; diff --git a/packages/flutter_order_details/lib/src/configuration/order_detail_configuration.dart b/packages/flutter_order_details/lib/src/configuration/order_detail_configuration.dart deleted file mode 100644 index def8d8c..0000000 --- a/packages/flutter_order_details/lib/src/configuration/order_detail_configuration.dart +++ /dev/null @@ -1,71 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// Configuration for the order detail screen. -class OrderDetailConfiguration { - /// Constructor for the order detail configuration. - const OrderDetailConfiguration({ - required this.shoppingService, - required this.onNextStep, - required this.onStepsCompleted, - required this.onCompleteOrderDetails, - this.pages, - this.translations = const OrderDetailTranslations(), - this.appBarBuilder, - this.nextbuttonBuilder, - this.orderSuccessBuilder, - }); - - /// The shopping service that is used - final ShoppingService shoppingService; - - /// The different steps that the user has to go through to complete the order. - /// Each step contains a list of fields that the user has to fill in. - final List Function(BuildContext context)? pages; - - /// Callback function that is called when the user has completed the order. - /// The result of the order is passed as an argument to the function. - final Function( - String shopId, - List products, - Map> value, - OrderDetailConfiguration configuration, - ) onStepsCompleted; - - /// Callback function that is called when the user has completed a step. - final Function( - int currentStep, - Map data, - FlutterFormController controller, - ) onNextStep; - - /// Localization for the order detail screen. - final OrderDetailTranslations translations; - - /// Optional app bar that you can pass to the order detail screen. - final PreferredSizeWidget? Function(BuildContext context, String title)? - appBarBuilder; - - /// Optional next button builder that you can pass to the order detail screen. - final Widget Function( - int currentStep, - // ignore: avoid_positional_boolean_parameters - bool checkingPages, - BuildContext context, - OrderDetailConfiguration configuration, - FlutterFormController controller, - )? nextbuttonBuilder; - - /// Optional builder for the order success screen. - final Widget Function( - BuildContext context, - OrderDetailConfiguration, - Map> orderDetails, - )? orderSuccessBuilder; - - /// This function is called after the order has been completed and - /// the success screen has been shown. - final Function(BuildContext context, OrderDetailConfiguration configuration) - onCompleteOrderDetails; -} diff --git a/packages/flutter_order_details/lib/src/configuration/order_detail_translations.dart b/packages/flutter_order_details/lib/src/configuration/order_detail_translations.dart deleted file mode 100644 index 55a306a..0000000 --- a/packages/flutter_order_details/lib/src/configuration/order_detail_translations.dart +++ /dev/null @@ -1,18 +0,0 @@ -/// Localizations for the order detail page. -class OrderDetailTranslations { - /// Constructor for the order detail localization. - const OrderDetailTranslations({ - this.nextButton = "Order", - this.completeButton = "Complete", - this.orderDetailsTitle = "Information", - }); - - /// Next button localization. - final String nextButton; - - /// Complete button localization. - final String completeButton; - - /// Title for the order details page. - final String orderDetailsTitle; -} diff --git a/packages/flutter_order_details/lib/src/order_detail_screen.dart b/packages/flutter_order_details/lib/src/order_detail_screen.dart deleted file mode 100644 index a87f354..0000000 --- a/packages/flutter_order_details/lib/src/order_detail_screen.dart +++ /dev/null @@ -1,68 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; -import "package:flutter_order_details/src/widgets/default_appbar.dart"; -import "package:flutter_order_details/src/widgets/default_next_button.dart"; -import "package:flutter_order_details/src/widgets/default_order_detail_pages.dart"; - -/// Order Detail Screen. -class OrderDetailScreen extends StatefulWidget { - /// Screen that builds all forms based on the configuration. - const OrderDetailScreen({ - required this.configuration, - super.key, - }); - - /// Configuration for the screen. - final OrderDetailConfiguration configuration; - - @override - State createState() => _OrderDetailScreenState(); -} - -class _OrderDetailScreenState extends State { - @override - Widget build(BuildContext context) { - var controller = FlutterFormController(); - return Scaffold( - appBar: widget.configuration.appBarBuilder?.call( - context, - widget.configuration.translations.orderDetailsTitle, - ) ?? - DefaultAppbar( - title: widget.configuration.translations.orderDetailsTitle, - ), - body: FlutterForm( - formController: controller, - options: FlutterFormOptions( - nextButton: (pageNumber, checkingPages) => - widget.configuration.nextbuttonBuilder?.call( - pageNumber, - checkingPages, - context, - widget.configuration, - controller, - ) ?? - DefaultNextButton( - controller: controller, - configuration: widget.configuration, - currentStep: pageNumber, - checkingPages: checkingPages, - ), - pages: widget.configuration.pages?.call(context) ?? - defaultPages(context, () { - setState(() {}); - }), - onFinished: (data) async { - widget.configuration.onStepsCompleted.call( - widget.configuration.shoppingService.shopService.selectedShop!.id, - widget.configuration.shoppingService.shoppingCartService.products, - data, - widget.configuration, - ); - }, - onNext: (step, data) {}, - ), - ), - ); - } -} diff --git a/packages/flutter_order_details/lib/src/widgets/default_appbar.dart b/packages/flutter_order_details/lib/src/widgets/default_appbar.dart deleted file mode 100644 index 57945ea..0000000 --- a/packages/flutter_order_details/lib/src/widgets/default_appbar.dart +++ /dev/null @@ -1,27 +0,0 @@ -import "package:flutter/material.dart"; - -/// Default appbar for the order details page. -class DefaultAppbar extends StatelessWidget implements PreferredSizeWidget { - /// Constructor for the default appbar for the order details page. - const DefaultAppbar({ - required this.title, - super.key, - }); - - /// Title of the appbar. - final String title; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return AppBar( - title: Text( - title, - style: theme.textTheme.headlineLarge, - ), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} diff --git a/packages/flutter_order_details/lib/src/widgets/default_next_button.dart b/packages/flutter_order_details/lib/src/widgets/default_next_button.dart deleted file mode 100644 index 290fb9d..0000000 --- a/packages/flutter_order_details/lib/src/widgets/default_next_button.dart +++ /dev/null @@ -1,69 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; - -/// Default next button for the order details page. -class DefaultNextButton extends StatelessWidget { - /// Constructor for the default next button for the order details page. - const DefaultNextButton({ - required this.controller, - required this.configuration, - required this.currentStep, - required this.checkingPages, - super.key, - }); - - /// Configuration for the order details page. - final OrderDetailConfiguration configuration; - - /// Controller for the form. - final FlutterFormController controller; - - /// Current step in the form. - final int currentStep; - - /// Whether the form is checking pages. - final bool checkingPages; - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - var nextButtonTexts = [ - "Choose date and time", - "Next", - "Next", - ]; - - return Align( - alignment: Alignment.bottomCenter, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 32), - child: SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: () async { - configuration.onNextStep( - currentStep, - controller.getCurrentStepResults(), - controller, - ); - }, - style: theme.filledButtonTheme.style?.copyWith( - backgroundColor: WidgetStateProperty.all( - theme.colorScheme.primary, - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 12, - ), - child: Text( - nextButtonTexts[currentStep], - style: theme.textTheme.displayLarge, - ), - ), - ), - ), - ), - ); - } -} diff --git a/packages/flutter_order_details/lib/src/widgets/default_order_detail_pages.dart b/packages/flutter_order_details/lib/src/widgets/default_order_detail_pages.dart deleted file mode 100644 index f2ec7b4..0000000 --- a/packages/flutter_order_details/lib/src/widgets/default_order_detail_pages.dart +++ /dev/null @@ -1,431 +0,0 @@ -import "package:animated_toggle/animated_toggle.dart"; -import "package:flutter/material.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; - -/// Default pages for the order details screen. -List defaultPages( - BuildContext context, - Function() onSwitched, -) { - var theme = Theme.of(context); - - var morningTimes = [ - "09:00", - "09:15", - "09:30", - "09:45", - "10:00", - "10:15", - "10:30", - "10:45", - "11:00", - "11:15", - "11:30", - "11:45", - ]; - - var afternoonTimes = [ - "12:00", - "12:15", - "12:30", - "12:45", - "13:00", - "13:15", - "13:30", - "13:45", - "14:00", - "14:15", - "14:30", - "14:45", - "15:00", - "15:15", - "15:30", - "15:45", - "16:00", - "16:15", - "16:30", - "16:45", - "17:00", - ]; - - InputDecoration inputDecoration(String hint) => InputDecoration( - hintStyle: theme.textTheme.bodySmall, - hintText: hint, - filled: true, - fillColor: Colors.white, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide.none, - ), - ); - InputDecoration dropdownInputDecoration(String hint) => InputDecoration( - hintStyle: theme.textTheme.bodySmall, - hintText: hint, - filled: true, - fillColor: Colors.white, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide.none, - ), - ); - var switchStatus = ValueNotifier(false); - var multipleChoiceController = FlutterFormInputMultipleChoiceController( - id: "multipleChoice", - mandatory: true, - ); - return [ - FlutterFormPage( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "What's your name?", - style: theme.textTheme.titleMedium, - ), - const SizedBox( - height: 4, - ), - FlutterFormInputPlainText( - decoration: inputDecoration("Name"), - style: theme.textTheme.bodySmall, - controller: FlutterFormInputPlainTextController( - id: "name", - mandatory: true, - ), - validationMessage: "Please enter your name", - ), - const SizedBox( - height: 16, - ), - Text( - "What's your address?", - style: theme.textTheme.titleMedium, - ), - const SizedBox( - height: 4, - ), - FlutterFormInputPlainText( - decoration: inputDecoration("Street and number"), - style: theme.textTheme.bodySmall, - controller: FlutterFormInputPlainTextController( - id: "street", - mandatory: true, - ), - validationMessage: "Please enter your address", - validator: (value) { - if (value == null || value.isEmpty) { - return "Please enter a street and house number"; - } - var regex = RegExp(r"^[A-Za-z]+\s[0-9]{1,3}$"); - if (!regex.hasMatch(value)) { - return "Invalid street and house number"; - } - return null; - }, - ), - const SizedBox( - height: 4, - ), - FlutterFormInputPlainText( - decoration: inputDecoration("Postal code"), - style: theme.textTheme.bodySmall, - controller: FlutterFormInputPlainTextController( - id: "postalCode", - mandatory: true, - ), - validationMessage: "Please enter your postal code", - validator: (value) { - if (value == null || value.isEmpty) { - return "Please enter a postal code"; - } - var regex = RegExp(r"^[0-9]{4}[A-Za-z]{2}$"); - if (!regex.hasMatch(value)) { - return "Invalid postal code format"; - } - return null; - }, - ), - const SizedBox( - height: 4, - ), - FlutterFormInputPlainText( - decoration: inputDecoration("City"), - style: theme.textTheme.bodySmall, - controller: FlutterFormInputPlainTextController( - id: "city", - mandatory: true, - ), - validationMessage: "Please enter your city", - ), - const SizedBox( - height: 16, - ), - Text( - "What's your phone number?", - style: theme.textTheme.titleMedium, - ), - const SizedBox( - height: 4, - ), - FlutterFormInputPhone( - numberFieldStyle: theme.textTheme.bodySmall, - textAlignVertical: TextAlignVertical.center, - decoration: inputDecoration("Phone number"), - controller: FlutterFormInputPhoneController( - id: "phone", - mandatory: true, - ), - validationMessage: "Please enter your phone number", - validator: (value) { - if (value == null || value.number!.isEmpty) { - return "Please enter a phone number"; - } - - // Remove any spaces or hyphens from the input - var phoneNumber = - value.number!.replaceAll(RegExp(r"\s+|-"), ""); - - // Check the length of the remaining digits - if (phoneNumber.length != 10 && phoneNumber.length != 11) { - return "Invalid phone number length"; - } - - // Check if all remaining characters are digits - if (!phoneNumber.substring(1).contains(RegExp(r"^[0-9]*$"))) { - return "Phone number can only contain digits"; - } - - // If all checks pass, return null (no error) - return null; - }, - ), - const SizedBox( - height: 16, - ), - Text( - "What's your email address?", - style: theme.textTheme.titleMedium, - ), - const SizedBox( - height: 4, - ), - FlutterFormInputEmail( - style: theme.textTheme.bodySmall, - decoration: inputDecoration("email address"), - controller: FlutterFormInputEmailController( - id: "email", - mandatory: true, - ), - validationMessage: "Please fill in a valid email address", - ), - const SizedBox( - height: 16, - ), - Text( - "Do you have any comments?", - style: theme.textTheme.titleMedium, - ), - const SizedBox( - height: 4, - ), - FlutterFormInputPlainText( - decoration: inputDecoration("Optional"), - style: theme.textTheme.bodySmall, - controller: FlutterFormInputPlainTextController( - id: "comments", - ), - validationMessage: "Please enter your email address", - ), - const SizedBox( - height: 100, - ), - ], - ), - ), - ), - ), - FlutterFormPage( - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Text( - "When and at what time would you like to pick up your order?", - style: theme.textTheme.titleMedium, - ), - ), - const SizedBox( - height: 4, - ), - FlutterFormInputDropdown( - icon: const Icon( - Icons.keyboard_arrow_down, - color: Colors.black, - ), - isDense: true, - decoration: dropdownInputDecoration("Select a day"), - validationMessage: "Please select a day", - controller: FlutterFormInputDropdownController( - id: "date", - mandatory: true, - ), - items: [ - DropdownMenuItem( - value: "Today", - child: Text( - "Today", - style: theme.textTheme.bodySmall, - ), - ), - DropdownMenuItem( - value: "Tomorrow", - child: Text( - "Tomorrow", - style: theme.textTheme.bodySmall, - ), - ), - ], - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: AnimatedToggle( - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - blurRadius: 5, - color: theme.colorScheme.primary.withOpacity(0.8), - ), - ], - color: Colors.white, - borderRadius: BorderRadius.circular(50), - ), - width: 280, - toggleColor: theme.colorScheme.primary, - onSwitch: (value) { - switchStatus.value = value; - onSwitched(); - }, - childLeft: Center( - child: Text( - "Morning", - style: theme.textTheme.titleSmall?.copyWith( - color: switchStatus.value - ? theme.colorScheme.primary - : Colors.white, - ), - ), - ), - childRight: Center( - child: Text( - "Afternoon", - style: theme.textTheme.titleSmall?.copyWith( - color: switchStatus.value - ? Colors.white - : theme.colorScheme.primary, - ), - ), - ), - ), - ), - const SizedBox( - height: 8, - ), - FlutterFormInputMultipleChoice( - validationMessage: "Select a Time", - controller: multipleChoiceController, - options: switchStatus.value ? afternoonTimes : morningTimes, - mainAxisSpacing: 5, - crossAxisSpacing: 5, - childAspectRatio: 2, - height: MediaQuery.of(context).size.height * 0.6, - builder: (context, index, selected, controller, options, state) => - GestureDetector( - onTap: () { - state.didChange(options[index]); - selected.value = index; - controller.onSaved(options[index]); - }, - child: Container( - decoration: BoxDecoration( - color: selected.value == index - ? Theme.of(context).colorScheme.primary - : Colors.white, - borderRadius: BorderRadius.circular(10), - ), - height: 40, - child: Center( - child: Text(options[index]), - ), - ), - ), - ), - ], - ), - ), - ), - FlutterFormPage( - child: Padding( - padding: const EdgeInsets.all(32), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Payment method", - style: theme.textTheme.titleMedium, - ), - const SizedBox( - height: 4, - ), - Text( - "Choose when you would like to to pay for the order.", - style: theme.textTheme.bodyMedium, - ), - const SizedBox( - height: 84, - ), - FlutterFormInputMultipleChoice( - crossAxisCount: 1, - mainAxisSpacing: 24, - crossAxisSpacing: 5, - childAspectRatio: 2, - height: 420, - controller: FlutterFormInputMultipleChoiceController( - id: "payment", - mandatory: true, - ), - options: const ["PAY NOW", "PAY AT THE CASHIER"], - builder: (context, index, selected, controller, options, state) => - GestureDetector( - onTap: () { - state.didChange(options[index]); - selected.value = index; - controller.onSaved(options[index]); - }, - child: Container( - decoration: BoxDecoration( - color: selected.value == index - ? Theme.of(context).colorScheme.primary - : Colors.white, - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: Theme.of(context).colorScheme.primary, - ), - ), - height: 40, - child: Center(child: Text(options[index])), - ), - ), - validationMessage: "Please select a payment method", - ), - ], - ), - ), - ), - ]; -} diff --git a/packages/flutter_order_details/lib/src/widgets/order_succes.dart b/packages/flutter_order_details/lib/src/widgets/order_succes.dart deleted file mode 100644 index 6abfd00..0000000 --- a/packages/flutter_order_details/lib/src/widgets/order_succes.dart +++ /dev/null @@ -1,222 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// Default order success widget. -class DefaultOrderSucces extends StatelessWidget { - /// Constructor for the DefaultOrderSucces. - const DefaultOrderSucces({ - required this.configuration, - required this.orderDetails, - super.key, - }); - - /// Configuration for the user-stor - final OrderDetailConfiguration configuration; - - /// Order details. - final Map> orderDetails; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - var discountedProducts = configuration - .shoppingService.productService.products - .where((product) => product.hasDiscount) - .toList(); - - return Scaffold( - appBar: AppBar( - title: Text( - "Confirmation", - style: theme.textTheme.headlineLarge, - ), - ), - body: SafeArea( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only( - left: 32, - top: 32, - right: 32, - ), - child: Column( - children: [ - Text( - "Success!", - style: theme.textTheme.titleMedium, - ), - const SizedBox( - height: 4, - ), - Text( - "Thank you ${orderDetails[0]!['name']} for your order!", - style: theme.textTheme.bodyMedium, - ), - const SizedBox( - height: 16, - ), - Text( - "The order was placed" - // ignore: lines_longer_than_80_chars - " at ${configuration.shoppingService.shopService.selectedShop?.name}." - " You can pick this" - " up ${orderDetails[1]!['date']} at" - " ${orderDetails[1]!['multipleChoice']}.", - style: theme.textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - const SizedBox( - height: 16, - ), - Text( - "If you want, you can place another order in this street.", - style: theme.textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - const SizedBox( - height: 32, - ), - Text( - "Weekly offers", - style: theme.textTheme.headlineSmall - ?.copyWith(color: Colors.black), - ), - const SizedBox( - height: 4, - ), - ], - ), - ), - SizedBox( - height: 272, - child: ListView( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - children: [ - const SizedBox(width: 32), - // _discount(context), - // const SizedBox(width: 8), - // _discount(context), - for (var product in discountedProducts) ...[ - _discount( - context, - product, - configuration.shoppingService.shopService.selectedShop!, - ), - const SizedBox( - width: 8, - ), - ], - const SizedBox(width: 32), - ], - ), - ), - const Spacer(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 60), - child: SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: () async { - configuration.onCompleteOrderDetails - .call(context, configuration); - }, - style: theme.filledButtonTheme.style?.copyWith( - backgroundColor: WidgetStateProperty.all( - theme.colorScheme.primary, - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 12, - ), - child: Text( - "Place another order", - style: theme.textTheme.displayLarge, - ), - ), - ), - ), - ), - ], - ), - ), - ); - } -} - -Widget _discount(BuildContext context, Product product, Shop shop) { - var theme = Theme.of(context); - return Container( - decoration: BoxDecoration( - border: Border.all( - color: Colors.black, - ), - borderRadius: BorderRadius.circular(10), - ), - width: MediaQuery.of(context).size.width - 64, - height: 200, - child: Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - 10, - ), - child: Image.network( - product.imageUrl, - width: double.infinity, - height: double.infinity, - fit: BoxFit.cover, - ), - ), - Container( - alignment: Alignment.centerLeft, - height: 38, - width: double.infinity, - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10), - topRight: Radius.circular(10), - ), - ), - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - shop.name, - style: theme.textTheme.headlineSmall?.copyWith( - color: Colors.white, - ), - ), - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(10), - bottomRight: Radius.circular(10), - ), - ), - alignment: Alignment.centerLeft, - width: MediaQuery.of(context).size.width, - height: 68, - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: Text( - "${product.name}, now for ${product.price.toStringAsFixed(2)}", - style: theme.textTheme.bodyMedium, - ), - ), - ), - ), - ], - ), - ); -} diff --git a/packages/flutter_order_details/pubspec.yaml b/packages/flutter_order_details/pubspec.yaml deleted file mode 100644 index 5e39831..0000000 --- a/packages/flutter_order_details/pubspec.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: flutter_order_details -description: "A Flutter module for order details." -version: 2.0.0 -publish_to: "none" - -environment: - sdk: ">=3.3.0 <4.0.0" - -dependencies: - flutter: - sdk: flutter - animated_toggle: - git: - url: https://github.com/Iconica-Development/flutter_animated_toggle - ref: 0.0.3 - flutter_form_wizard: - git: - url: https://github.com/Iconica-Development/flutter_form_wizard - ref: 6.5.0 - flutter_shopping_interface: - git: - url: https://github.com/Iconica-Development/flutter_shopping - path: packages/flutter_shopping_interface - ref: 2.0.0 - collection: ^1.18.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 - -flutter: diff --git a/packages/flutter_product_page/README.md b/packages/flutter_product_page/README.md deleted file mode 100644 index fd0ed4d..0000000 --- a/packages/flutter_product_page/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# flutter_product_page - -This component allows you to easily create and manage the products for any shop. Easily highlight a specific product -and automatically see your products categorized. This package allows users to gather more information about a product, -add it to a custom implementable shopping cart and even navigate to your own shopping cart. - -This component is very customizable, it allows you to adjust basically everything while providing clean defaults. - -## Features - -* Easily navigate between different shops, -* Show users a highlighted product, -* Integrate with your own shopping cart, -* Automatically categorized products, powered by the `flutter_nested_categories` package that you have full control over, even in this component. - -## Usage - -First, you must implement your own `Shop` and `Product` classes. Your shop class must extend from the `ProductPageShop` class provided by this module. Your `Product` class should extend from the `Product` class provided by this module. - -Next, you can create a `ProductPage` or a `ProductPageScreen`. The choice for the former is when you do not want to create a new Scaffold and the latter for when you want to create a new Scaffold. - -To show the page, you must configure what you want to show. Both the `ProductPage` and the `ProductPageScreen` take a parameter that is a `ProductPageConfiguration`. This allows you for a lot of customizability, including what shops there are and what products to show. - -For a more detailed example you can see the [example](https://github.com/Iconica-Development/flutter_product_page/tree/main/example). - -Or, you could run the example yourself: -``` -git clone https://github.com/Iconica-Development/flutter_product_page.git - -cd flutter_product_page - -cd example - -flutter run -``` - -## Issues - -Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_product_page) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl). - -## Want to contribute - -If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_product_page/pulls). - -## Author - -This flutter_product_page for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at diff --git a/packages/flutter_product_page/analysis_options.yaml b/packages/flutter_product_page/analysis_options.yaml deleted file mode 100644 index 0736605..0000000 --- a/packages/flutter_product_page/analysis_options.yaml +++ /dev/null @@ -1,9 +0,0 @@ -include: package:flutter_iconica_analysis/components_options.yaml - -# Possible to overwrite the rules from the package - -analyzer: - exclude: - -linter: - rules: diff --git a/packages/flutter_product_page/lib/flutter_product_page.dart b/packages/flutter_product_page/lib/flutter_product_page.dart deleted file mode 100644 index f15fb25..0000000 --- a/packages/flutter_product_page/lib/flutter_product_page.dart +++ /dev/null @@ -1,8 +0,0 @@ -/// Module for creating a product page with a list of products and a -/// detailed view of each product. -library flutter_product_page; - -export "src/configuration/product_page_configuration.dart"; -export "src/configuration/product_page_shop_selector_style.dart"; -export "src/configuration/product_page_translations.dart"; -export "src/product_page_screen.dart"; diff --git a/packages/flutter_product_page/lib/src/category_selection_screen.dart b/packages/flutter_product_page/lib/src/category_selection_screen.dart deleted file mode 100644 index 0f3649f..0000000 --- a/packages/flutter_product_page/lib/src/category_selection_screen.dart +++ /dev/null @@ -1,67 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; - -/// Category selection screen. -class CategorySelectionScreen extends StatelessWidget { - /// Constructor for the category selection screen. - const CategorySelectionScreen({ - required this.configuration, - super.key, - }); - - /// Configuration for the product page. - final ProductPageConfiguration configuration; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Scaffold( - appBar: AppBar( - leading: const SizedBox.shrink(), - title: Text( - "filter", - style: theme.textTheme.headlineLarge, - ), - actions: [ - IconButton( - onPressed: () async { - Navigator.of(context).pop(); - }, - icon: const Icon(Icons.close), - ), - ], - ), - body: ListenableBuilder( - listenable: configuration.shoppingService.productService, - builder: (context, _) => Column( - children: [ - ...configuration.shoppingService.productService.getCategories().map( - (category) { - var isChecked = configuration - .shoppingService.productService.selectedCategories - .contains(category); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: CheckboxListTile( - activeColor: theme.colorScheme.primary, - controlAffinity: ListTileControlAffinity.leading, - value: isChecked, - onChanged: (value) { - configuration.shoppingService.productService - .selectCategory(category); - }, - shape: const UnderlineInputBorder(), - title: Text( - category, - style: theme.textTheme.bodyMedium, - ), - ), - ); - }, - ), - ], - ), - ), - ); - } -} diff --git a/packages/flutter_product_page/lib/src/configuration/product_page_configuration.dart b/packages/flutter_product_page/lib/src/configuration/product_page_configuration.dart deleted file mode 100644 index b9ef07a..0000000 --- a/packages/flutter_product_page/lib/src/configuration/product_page_configuration.dart +++ /dev/null @@ -1,173 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; -import "package:flutter_product_page/src/widgets/product_item_popup.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// Configuration for the product page. -class ProductPageConfiguration { - /// Constructor for the product page configuration. - ProductPageConfiguration({ - required this.shoppingService, - required this.shops, - required this.getProducts, - required this.onAddToCart, - required this.onNavigateToShoppingCart, - required this.getProductsInShoppingCart, - this.shoppingCartButtonBuilder, - this.initialShopId, - this.productBuilder, - this.onShopSelectionChange, - this.translations = const ProductPageTranslations(), - this.shopSelectorStyle = ShopSelectorStyle.row, - this.pagePadding = const EdgeInsets.all(4), - this.appBarBuilder, - this.bottomNavigationBar, - this.onProductDetail, - this.discountDescription, - this.noContentBuilder, - this.errorBuilder, - this.shopselectorBuilder, - this.discountBuilder, - this.categoryListBuilder, - this.selectedCategoryBuilder, - }) { - onProductDetail ??= _onProductDetail; - discountDescription ??= _defaultDiscountDescription; - } - - /// The shopping service that is used - final ShoppingService shoppingService; - - /// The shop that is initially selected. - final String? initialShopId; - - /// A list of all the shops that the user must be able to navigate from. - final Future> Function() shops; - - /// A function that returns all the products that belong to a certain shop. - /// The function must return a [List]. - final Future> Function(Shop shop) getProducts; - - /// The localizations for the product page. - final ProductPageTranslations translations; - - /// Builder for the product item. These items will be displayed in the list - /// for each product in their seperated category. This builder should only - /// build the widget for one specific product. This builder has a default - /// in-case the developer does not override it. - final Widget Function( - BuildContext context, - Product product, - ProductPageConfiguration configuration, - )? productBuilder; - - /// The builder for the product popup. This builder should return a widget - Function( - BuildContext context, - Product product, - String closeText, - )? onProductDetail; - - /// The builder for the shopping cart. This builder should return a widget - /// that navigates to the shopping cart overview page. - final Widget Function( - BuildContext context, - ProductPageConfiguration configuration, - )? shoppingCartButtonBuilder; - - /// The function that returns the discount description for a product. - String Function( - Product product, - )? discountDescription; - - /// This function must be implemented by the developer and should handle the - /// adding of a product to the cart. - Function(Product product) onAddToCart; - - /// This function gets executed when the user changes the shop selection. - /// This function always fires upon first load with the initial shop as well. - final Function(Shop shop)? onShopSelectionChange; - - /// This function must be implemented by the developer and should handle the - /// navigation to the shopping cart overview page. - final int Function() getProductsInShoppingCart; - - /// This function must be implemented by the developer and should handle the - /// navigation to the shopping cart overview page. - final Function() onNavigateToShoppingCart; - - /// The style of the shop selector. - final ShopSelectorStyle shopSelectorStyle; - - /// The padding for the page. - final EdgeInsets pagePadding; - - /// Optional app bar that you can pass to the product page screen. - final Widget? bottomNavigationBar; - - /// Optional app bar that you can pass to the order detail screen. - final PreferredSizeWidget Function(BuildContext context)? appBarBuilder; - - /// Builder for the no content widget. This builder is used when there is no - /// content to display. - final Widget Function( - BuildContext context, - )? noContentBuilder; - - /// Builder for the error widget. This builder is used when there is an error - /// to display. - final Widget Function( - BuildContext context, - Object? error, - )? errorBuilder; - - /// Builder for the shop selector. This builder is used to build the shop - /// selector that will be displayed in the product page. - final Widget Function( - BuildContext context, - ProductPageConfiguration configuration, - List shops, - Function(Shop shop) onShopSelectionChange, - )? shopselectorBuilder; - - /// Builder for the discount widget. This builder is used to build the - /// discount widget that will be displayed in the product page. - final Widget Function( - BuildContext context, - ProductPageConfiguration configuration, - List discountedProducts, - )? discountBuilder; - - /// Builder for the list of items that are displayed in the product page. - final Widget Function( - BuildContext context, - ProductPageConfiguration configuration, - List products, - )? categoryListBuilder; - - /// Builder for the list of selected categories - final Widget Function(ProductPageConfiguration configuration)? - selectedCategoryBuilder; -} - -Future _onProductDetail( - BuildContext context, - Product product, - String closeText, -) async { - var theme = Theme.of(context); - - await showModalBottomSheet( - context: context, - backgroundColor: theme.colorScheme.surface, - builder: (context) => ProductItemPopup( - product: product, - closeText: closeText, - ), - ); -} - -String _defaultDiscountDescription( - Product product, -) => - "${product.name}, now for ${product.discountPrice} each"; diff --git a/packages/flutter_product_page/lib/src/configuration/product_page_shop_selector_style.dart b/packages/flutter_product_page/lib/src/configuration/product_page_shop_selector_style.dart deleted file mode 100644 index e37c07a..0000000 --- a/packages/flutter_product_page/lib/src/configuration/product_page_shop_selector_style.dart +++ /dev/null @@ -1,8 +0,0 @@ -/// Style for the shop selector in the product page. -enum ShopSelectorStyle { - /// Shops are displayed in a row. - row, - - /// Shops are displayed in a wrap. - spacedWrap, -} diff --git a/packages/flutter_product_page/lib/src/configuration/product_page_translations.dart b/packages/flutter_product_page/lib/src/configuration/product_page_translations.dart deleted file mode 100644 index ff6bfd2..0000000 --- a/packages/flutter_product_page/lib/src/configuration/product_page_translations.dart +++ /dev/null @@ -1,30 +0,0 @@ -/// Localization for the product page -class ProductPageTranslations { - /// Default constructor - const ProductPageTranslations({ - this.navigateToShoppingCart = "View shopping cart", - this.discountTitle = "Weekly offer", - this.failedToLoadImageExplenation = "Failed to load image", - this.close = "Close", - this.categoryItemListTitle = "What would you like to order", - this.appBarTitle = "ProductPage", - }); - - /// Message to navigate to the shopping cart - final String navigateToShoppingCart; - - /// Title for the discount - final String discountTitle; - - /// Explenation when the image failed to load - final String failedToLoadImageExplenation; - - /// Close button for the product page - final String close; - - /// Title for the category item list - final String categoryItemListTitle; - - /// Title for the app bar - final String appBarTitle; -} diff --git a/packages/flutter_product_page/lib/src/product_page_screen.dart b/packages/flutter_product_page/lib/src/product_page_screen.dart deleted file mode 100644 index 6cc5303..0000000 --- a/packages/flutter_product_page/lib/src/product_page_screen.dart +++ /dev/null @@ -1,306 +0,0 @@ -import "package:collection/collection.dart"; -import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; -import "package:flutter_product_page/src/services/category_service.dart"; -import "package:flutter_product_page/src/widgets/defaults/default_appbar.dart"; -import "package:flutter_product_page/src/widgets/defaults/default_error.dart"; -import "package:flutter_product_page/src/widgets/defaults/default_no_content.dart"; -import "package:flutter_product_page/src/widgets/defaults/default_shopping_cart_button.dart"; -import "package:flutter_product_page/src/widgets/defaults/selected_categories.dart"; -import "package:flutter_product_page/src/widgets/shop_selector.dart"; -import "package:flutter_product_page/src/widgets/weekly_discount.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// A page that displays products. -class ProductPageScreen extends StatefulWidget { - /// Constructor for the product page. - const ProductPageScreen({ - required this.configuration, - super.key, - }); - - /// Configuration for the product page. - final ProductPageConfiguration configuration; - - @override - State createState() => _ProductPageScreenState(); -} - -class _ProductPageScreenState extends State { - @override - Widget build(BuildContext context) => Scaffold( - appBar: widget.configuration.appBarBuilder?.call(context) ?? - DefaultAppbar( - configuration: widget.configuration, - ), - bottomNavigationBar: widget.configuration.bottomNavigationBar, - body: SafeArea( - child: Padding( - padding: widget.configuration.pagePadding, - child: FutureBuilder( - // ignore: discarded_futures - future: widget.configuration.shops(), - builder: (context, snapshot) { - List? shops; - - if (snapshot.connectionState == ConnectionState.waiting) { - return const Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center(child: CircularProgressIndicator.adaptive()), - ], - ); - } - - if (snapshot.hasError) { - return widget.configuration.errorBuilder?.call( - context, - snapshot.error, - ) ?? - DefaultError( - error: snapshot.error, - ); - } - - shops = snapshot.data; - - if (shops == null || shops.isEmpty) { - return widget.configuration.errorBuilder?.call( - context, - snapshot.error, - ) ?? - DefaultError(error: snapshot.error); - } - - if (widget.configuration.initialShopId != null) { - var initialShop = shops.firstWhereOrNull( - (shop) => shop.id == widget.configuration.initialShopId, - ); - if (initialShop != null) { - widget.configuration.shoppingService.shopService.selectShop( - initialShop, - ); - } else { - widget.configuration.shoppingService.shopService.selectShop( - shops.first, - ); - } - } else { - widget.configuration.shoppingService.shopService.selectShop( - shops.first, - ); - } - return _ProductPageContent( - configuration: widget.configuration, - shops: shops, - ); - }, - ), - ), - ), - ); -} - -class _ProductPageContent extends StatefulWidget { - const _ProductPageContent({ - required this.configuration, - required this.shops, - }); - - final ProductPageConfiguration configuration; - - final List shops; - - @override - State<_ProductPageContent> createState() => _ProductPageContentState(); -} - -class _ProductPageContentState extends State<_ProductPageContent> { - @override - Widget build(BuildContext context) => Stack( - children: [ - SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // shop selector - widget.configuration.shopselectorBuilder?.call( - context, - widget.configuration, - widget.shops, - widget - .configuration.shoppingService.shopService.selectShop, - ) ?? - ShopSelector( - configuration: widget.configuration, - shops: widget.shops, - onTap: (shop) { - widget.configuration.shoppingService.shopService - .selectShop(shop); - }, - ), - // selected categories - widget.configuration.selectedCategoryBuilder?.call( - widget.configuration, - ) ?? - SelectedCategories( - configuration: widget.configuration, - ), - // products - _ShopContents( - configuration: widget.configuration, - ), - ], - ), - ), - - // button - Align( - alignment: Alignment.bottomCenter, - child: widget.configuration.shoppingCartButtonBuilder != null - ? widget.configuration.shoppingCartButtonBuilder!( - context, - widget.configuration, - ) - : DefaultShoppingCartButton( - configuration: widget.configuration, - ), - ), - ], - ); -} - -class _ShopContents extends StatefulWidget { - const _ShopContents({ - required this.configuration, - }); - - final ProductPageConfiguration configuration; - - @override - State<_ShopContents> createState() => _ShopContentsState(); -} - -class _ShopContentsState extends State<_ShopContents> { - @override - void initState() { - widget.configuration.shoppingService.shopService.addListener(_listen); - super.initState(); - } - - @override - void dispose() { - widget.configuration.shoppingService.shopService.removeListener(_listen); - super.dispose(); - } - - void _listen() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Padding( - padding: EdgeInsets.symmetric( - horizontal: widget.configuration.pagePadding.horizontal, - ), - child: FutureBuilder( - // ignore: discarded_futures - future: widget.configuration.getProducts( - widget.configuration.shoppingService.shopService.selectedShop!, - ), - builder: (context, snapshot) { - List productPageContent; - - if (snapshot.connectionState == ConnectionState.waiting) { - return SizedBox( - height: MediaQuery.of(context).size.height * 0.7, - child: const Center( - child: CircularProgressIndicator.adaptive(), - ), - ); - } - - if (snapshot.hasError) { - if (widget.configuration.errorBuilder != null) { - return widget.configuration.errorBuilder!( - context, - snapshot.error, - ); - } else { - return DefaultError(error: snapshot.error); - } - } - - productPageContent = - widget.configuration.shoppingService.productService.products; - - if (productPageContent.isEmpty) { - return widget.configuration.noContentBuilder?.call(context) ?? - const DefaultNoContent(); - } - - var discountedproducts = productPageContent - .where((product) => product.hasDiscount) - .toList(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Discounted product - if (discountedproducts.isNotEmpty) ...[ - widget.configuration.discountBuilder?.call( - context, - widget.configuration, - discountedproducts, - ) ?? - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: WeeklyDiscount( - configuration: widget.configuration, - product: discountedproducts.first, - ), - ), - ], - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 24), - child: Text( - widget.configuration.translations.categoryItemListTitle, - style: theme.textTheme.titleLarge, - textAlign: TextAlign.start, - ), - ), - - widget.configuration.categoryListBuilder?.call( - context, - widget.configuration, - productPageContent, - ) ?? - Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), - child: Column( - children: [ - // Products - - getCategoryList( - context, - widget.configuration, - widget.configuration.shoppingService.productService - .products, - ), - - // Bottom padding so the last product is not cut off - // by the to shopping cart button. - const SizedBox(height: 48), - ], - ), - ), - ], - ); - }, - ), - ); - } -} diff --git a/packages/flutter_product_page/lib/src/services/category_service.dart b/packages/flutter_product_page/lib/src/services/category_service.dart deleted file mode 100644 index 4081d53..0000000 --- a/packages/flutter_product_page/lib/src/services/category_service.dart +++ /dev/null @@ -1,60 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_nested_categories/flutter_nested_categories.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; -import "package:flutter_product_page/src/widgets/defaults/default_product_item.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// Generates a [CategoryList] from a list of [Product]s and a -/// [ProductPageConfiguration]. -Widget getCategoryList( - BuildContext context, - ProductPageConfiguration configuration, - List products, -) { - var theme = Theme.of(context); - var categorizedProducts = >{}; - for (var product in products) { - if (!categorizedProducts.containsKey(product.category)) { - categorizedProducts[product.category] = []; - } - categorizedProducts[product.category]?.add(product); - } - - // Create Category instances - var categories = []; - categorizedProducts.forEach((categoryName, productList) { - var productWidgets = productList - .map( - (product) => - configuration.productBuilder - ?.call(context, product, configuration) ?? - DefaultProductItem( - product: product, - onAddToCart: configuration.onAddToCart, - onProductDetail: configuration.onProductDetail!, - translations: configuration.translations, - ), - ) - .toList(); - var category = Category( - name: categoryName, - content: productWidgets, - ); - categories.add(category); - }); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (var category in categories) ...[ - Text( - category.name!, - style: theme.textTheme.titleMedium, - ), - Column( - children: category.content, - ), - const SizedBox(height: 16), - ], - ], - ); -} diff --git a/packages/flutter_product_page/lib/src/widgets/defaults/default_appbar.dart b/packages/flutter_product_page/lib/src/widgets/defaults/default_appbar.dart deleted file mode 100644 index e81cc2d..0000000 --- a/packages/flutter_product_page/lib/src/widgets/defaults/default_appbar.dart +++ /dev/null @@ -1,45 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; -import "package:flutter_product_page/src/category_selection_screen.dart"; - -/// Default appbar for the product page. -class DefaultAppbar extends StatelessWidget implements PreferredSizeWidget { - /// Constructor for the default appbar for the product page. - const DefaultAppbar({ - required this.configuration, - super.key, - }); - - /// Configuration for the product page. - final ProductPageConfiguration configuration; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - return AppBar( - leading: IconButton(onPressed: () {}, icon: const Icon(Icons.person)), - actions: [ - IconButton( - onPressed: () async { - await Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => CategorySelectionScreen( - configuration: configuration, - ), - ), - ); - }, - icon: const Icon(Icons.filter_alt), - ), - ], - title: Text( - configuration.translations.appBarTitle, - style: theme.textTheme.headlineLarge, - ), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} diff --git a/packages/flutter_product_page/lib/src/widgets/defaults/default_error.dart b/packages/flutter_product_page/lib/src/widgets/defaults/default_error.dart deleted file mode 100644 index 8ddf940..0000000 --- a/packages/flutter_product_page/lib/src/widgets/defaults/default_error.dart +++ /dev/null @@ -1,24 +0,0 @@ -import "package:flutter/material.dart"; - -/// Default error widget. -class DefaultError extends StatelessWidget { - /// Constructor for the default error widget. - const DefaultError({ - super.key, - this.error, - }); - - /// Error that occurred. - final Object? error; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Center( - child: Text( - "Error: $error", - style: theme.textTheme.titleLarge, - ), - ); - } -} diff --git a/packages/flutter_product_page/lib/src/widgets/defaults/default_no_content.dart b/packages/flutter_product_page/lib/src/widgets/defaults/default_no_content.dart deleted file mode 100644 index 00fa21f..0000000 --- a/packages/flutter_product_page/lib/src/widgets/defaults/default_no_content.dart +++ /dev/null @@ -1,18 +0,0 @@ -import "package:flutter/material.dart"; - -/// Default no content widget. -class DefaultNoContent extends StatelessWidget { - /// Constructor for the default no content widget. - const DefaultNoContent({super.key}); - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Center( - child: Text( - "No content", - style: theme.textTheme.titleLarge, - ), - ); - } -} diff --git a/packages/flutter_product_page/lib/src/widgets/defaults/default_product_item.dart b/packages/flutter_product_page/lib/src/widgets/defaults/default_product_item.dart deleted file mode 100644 index 859592d..0000000 --- a/packages/flutter_product_page/lib/src/widgets/defaults/default_product_item.dart +++ /dev/null @@ -1,191 +0,0 @@ -import "package:cached_network_image/cached_network_image.dart"; -import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; -import "package:skeletonizer/skeletonizer.dart"; - -/// Product item widget. -class DefaultProductItem extends StatelessWidget { - /// Constructor for the product item widget. - const DefaultProductItem({ - required this.product, - required this.onProductDetail, - required this.onAddToCart, - required this.translations, - super.key, - }); - - /// Product to display. - final Product product; - - /// Function to call when the product detail is requested. - final Function( - BuildContext context, - Product selectedProduct, - String closeText, - ) onProductDetail; - - /// Function to call when the product is added to the cart. - final Function(Product selectedProduct) onAddToCart; - - /// Localizations for the product page. - final ProductPageTranslations translations; - - /// Size of the product image. - static const double imageSize = 44; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - var loadingImageSkeleton = const Skeletonizer.zone( - child: SizedBox(width: imageSize, height: imageSize, child: Bone.icon()), - ); - - var productIcon = ClipRRect( - borderRadius: BorderRadius.circular(4), - child: CachedNetworkImage( - imageUrl: product.imageUrl, - width: imageSize, - height: imageSize, - fit: BoxFit.cover, - placeholder: (context, url) => loadingImageSkeleton, - errorWidget: (context, url, error) => Tooltip( - message: translations.failedToLoadImageExplenation, - child: Container( - width: 48, - height: 48, - alignment: Alignment.center, - child: const Icon( - Icons.error_outline_sharp, - color: Colors.red, - ), - ), - ), - ), - ); - - var productName = Padding( - padding: const EdgeInsets.only(left: 8), - child: Container( - constraints: const BoxConstraints(maxWidth: 150), - child: Text( - product.name, - style: theme.textTheme.titleMedium, - ), - ), - ); - - var productInformationIcon = Padding( - padding: const EdgeInsets.only(left: 4), - child: IconButton( - onPressed: () => onProductDetail( - context, - product, - translations.close, - ), - icon: Icon( - Icons.info_outline, - color: theme.colorScheme.primary, - ), - ), - ); - - var productInteraction = Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - _PriceLabel( - product: product, - ), - _AddToCardButton( - product: product, - onAddToCart: onAddToCart, - ), - ], - ); - - return Padding( - padding: const EdgeInsets.only(top: 12, bottom: 8), - child: Row( - children: [ - productIcon, - productName, - productInformationIcon, - const Spacer(), - productInteraction, - ], - ), - ); - } -} - -class _PriceLabel extends StatelessWidget { - const _PriceLabel({ - required this.product, - }); - - final Product product; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - return Row( - children: [ - if (product.hasDiscount) ...[ - Text( - product.price.toStringAsFixed(2), - style: theme.textTheme.bodySmall?.copyWith( - decoration: TextDecoration.lineThrough, - ), - textAlign: TextAlign.center, - ), - const SizedBox(width: 4), - ], - Text( - product.hasDiscount - ? product.discountPrice!.toStringAsFixed(2) - : product.price.toStringAsFixed(2), - style: theme.textTheme.bodySmall, - textAlign: TextAlign.center, - ), - ], - ); - } -} - -class _AddToCardButton extends StatelessWidget { - const _AddToCardButton({ - required this.product, - required this.onAddToCart, - }); - - final Product product; - final Function(Product product) onAddToCart; - - static const double boxSize = 29; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Container( - decoration: BoxDecoration( - color: theme.colorScheme.primary, - borderRadius: BorderRadius.circular(4), - ), - width: boxSize, - height: boxSize, - child: Center( - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon( - Icons.add, - color: Colors.white, - size: 20, - ), - onPressed: () => onAddToCart(product), - ), - ), - ); - } -} diff --git a/packages/flutter_product_page/lib/src/widgets/defaults/default_shopping_cart_button.dart b/packages/flutter_product_page/lib/src/widgets/defaults/default_shopping_cart_button.dart deleted file mode 100644 index 1645ca9..0000000 --- a/packages/flutter_product_page/lib/src/widgets/defaults/default_shopping_cart_button.dart +++ /dev/null @@ -1,71 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; - -/// Default shopping cart button for the product page. -class DefaultShoppingCartButton extends StatefulWidget { - /// Constructor for the default shopping cart button for the product page. - const DefaultShoppingCartButton({ - required this.configuration, - super.key, - }); - - /// Configuration for the product page. - final ProductPageConfiguration configuration; - - @override - State createState() => - _DefaultShoppingCartButtonState(); -} - -class _DefaultShoppingCartButtonState extends State { - @override - void initState() { - super.initState(); - widget.configuration.shoppingService.shoppingCartService - .addListener(_listen); - } - - @override - void dispose() { - widget.configuration.shoppingService.shoppingCartService - .removeListener(_listen); - super.dispose(); - } - - void _listen() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 60), - child: SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: widget.configuration.shoppingService.shoppingCartService - .products.isNotEmpty - ? widget.configuration.onNavigateToShoppingCart - : null, - style: theme.filledButtonTheme.style?.copyWith( - backgroundColor: WidgetStateProperty.all( - theme.colorScheme.primary, - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 12, - ), - child: Text( - widget.configuration.translations.navigateToShoppingCart, - style: theme.textTheme.displayLarge, - ), - ), - ), - ), - ); - } -} diff --git a/packages/flutter_product_page/lib/src/widgets/defaults/selected_categories.dart b/packages/flutter_product_page/lib/src/widgets/defaults/selected_categories.dart deleted file mode 100644 index 2e5ee9a..0000000 --- a/packages/flutter_product_page/lib/src/widgets/defaults/selected_categories.dart +++ /dev/null @@ -1,72 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; - -/// Selected categories. -class SelectedCategories extends StatefulWidget { - /// Constructor for the selected categories. - const SelectedCategories({ - required this.configuration, - super.key, - }); - - /// Configuration for the product page. - final ProductPageConfiguration configuration; - - @override - State createState() => _SelectedCategoriesState(); -} - -class _SelectedCategoriesState extends State { - @override - void initState() { - widget.configuration.shoppingService.productService.addListener(_listen); - super.initState(); - } - - @override - void dispose() { - widget.configuration.shoppingService.productService.removeListener(_listen); - super.dispose(); - } - - void _listen() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.only(left: 4), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (var category in widget.configuration.shoppingService - .productService.selectedCategories) ...[ - Padding( - padding: const EdgeInsets.only(right: 8), - child: Chip( - backgroundColor: theme.colorScheme.primary, - deleteIcon: const Icon( - Icons.close, - color: Colors.white, - ), - onDeleted: () { - widget.configuration.shoppingService.productService - .selectCategory(category); - }, - label: Text( - category, - style: theme.textTheme.bodyMedium - ?.copyWith(color: Colors.white), - ), - ), - ), - ], - ], - ), - ), - ); - } -} diff --git a/packages/flutter_product_page/lib/src/widgets/horizontal_list_items.dart b/packages/flutter_product_page/lib/src/widgets/horizontal_list_items.dart deleted file mode 100644 index bdd6495..0000000 --- a/packages/flutter_product_page/lib/src/widgets/horizontal_list_items.dart +++ /dev/null @@ -1,78 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// Horizontal list of items. -class HorizontalListItems extends StatelessWidget { - /// Constructor for the horizontal list of items. - const HorizontalListItems({ - required this.shops, - required this.selectedItem, - required this.onTap, - this.paddingBetweenButtons = 2.0, - this.paddingOnButtons = 6, - super.key, - }); - - /// List of items. - final List shops; - - /// Selected item. - final String selectedItem; - - /// Padding between the buttons. - final double paddingBetweenButtons; - - /// Padding on the buttons. - final double paddingOnButtons; - - /// Callback when an item is tapped. - final Function(Shop shop) onTap; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - return Padding( - padding: const EdgeInsets.only( - top: 4, - ), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: shops - .map( - (shop) => Padding( - padding: EdgeInsets.only(right: paddingBetweenButtons), - child: InkWell( - onTap: () => onTap(shop), - child: Container( - decoration: BoxDecoration( - color: shop.id == selectedItem - ? theme.colorScheme.primary - : Colors.white, - borderRadius: BorderRadius.circular(4), - border: Border.all( - color: theme.colorScheme.primary, - width: 1, - ), - ), - padding: EdgeInsets.all(paddingOnButtons), - child: Text( - shop.name, - style: shop.id == selectedItem - ? theme.textTheme.bodyMedium?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - ) - : theme.textTheme.bodyMedium, - ), - ), - ), - ), - ) - .toList(), - ), - ), - ); - } -} diff --git a/packages/flutter_product_page/lib/src/widgets/product_item_popup.dart b/packages/flutter_product_page/lib/src/widgets/product_item_popup.dart deleted file mode 100644 index 77db847..0000000 --- a/packages/flutter_product_page/lib/src/widgets/product_item_popup.dart +++ /dev/null @@ -1,65 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// A popup that displays the product item. -class ProductItemPopup extends StatelessWidget { - /// Constructor for the product item popup. - const ProductItemPopup({ - required this.product, - required this.closeText, - super.key, - }); - - /// The product to display. - final Product product; - - /// Configuration for the product page. - final String closeText; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(32), - child: SizedBox( - width: double.infinity, - child: Column( - children: [ - Text( - product.description, - style: theme.textTheme.bodySmall, - textAlign: TextAlign.center, - ), - Padding( - padding: const EdgeInsets.only(top: 20, left: 40, right: 40), - child: SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: () => Navigator.of(context).pop(), - style: theme.filledButtonTheme.style?.copyWith( - backgroundColor: WidgetStateProperty.all( - theme.colorScheme.primary, - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Text( - closeText, - style: theme.textTheme.displayLarge, - ), - ), - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/packages/flutter_product_page/lib/src/widgets/shop_selector.dart b/packages/flutter_product_page/lib/src/widgets/shop_selector.dart deleted file mode 100644 index 4ed4090..0000000 --- a/packages/flutter_product_page/lib/src/widgets/shop_selector.dart +++ /dev/null @@ -1,85 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; -import "package:flutter_product_page/src/widgets/horizontal_list_items.dart"; -import "package:flutter_product_page/src/widgets/spaced_wrap.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// Shop selector widget that displays a list to navigate between shops. -class ShopSelector extends StatefulWidget { - /// Constructor for the shop selector. - const ShopSelector({ - required this.configuration, - required this.shops, - required this.onTap, - this.paddingBetweenButtons = 4, - this.paddingOnButtons = 8, - super.key, - }); - - /// Configuration for the product page. - final ProductPageConfiguration configuration; - - /// Service for the selected shop. - - /// List of shops. - final List shops; - - /// Callback when a shop is tapped. - final Function(Shop shop) onTap; - - /// Padding between the buttons. - final double paddingBetweenButtons; - - /// Padding on the buttons. - final double paddingOnButtons; - - @override - State createState() => _ShopSelectorState(); -} - -class _ShopSelectorState extends State { - @override - void initState() { - widget.configuration.shoppingService.shopService.addListener(_listen); - super.initState(); - } - - @override - void dispose() { - widget.configuration.shoppingService.shopService.removeListener(_listen); - super.dispose(); - } - - void _listen() { - setState(() {}); - } - - @override - Widget build(BuildContext context) { - if (widget.shops.length == 1) { - return const SizedBox.shrink(); - } - - if (widget.configuration.shopSelectorStyle == - ShopSelectorStyle.spacedWrap) { - return SpacedWrap( - shops: widget.shops, - selectedItem: - widget.configuration.shoppingService.shopService.selectedShop!.id, - onTap: widget.onTap, - width: MediaQuery.of(context).size.width - (16 * 2), - paddingBetweenButtons: widget.paddingBetweenButtons, - paddingOnButtons: widget.paddingOnButtons, - ); - } - - return HorizontalListItems( - shops: widget.shops, - selectedItem: - widget.configuration.shoppingService.shopService.selectedShop!.id, - onTap: widget.onTap, - paddingBetweenButtons: widget.paddingBetweenButtons, - paddingOnButtons: widget.paddingOnButtons, - ); - } -} diff --git a/packages/flutter_product_page/lib/src/widgets/spaced_wrap.dart b/packages/flutter_product_page/lib/src/widgets/spaced_wrap.dart deleted file mode 100644 index 24b19c5..0000000 --- a/packages/flutter_product_page/lib/src/widgets/spaced_wrap.dart +++ /dev/null @@ -1,76 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// SpacedWrap is a widget that wraps a list of items that are spaced out and -/// fill the available width. -class SpacedWrap extends StatelessWidget { - /// Creates a [SpacedWrap]. - const SpacedWrap({ - required this.shops, - required this.onTap, - required this.width, - this.paddingBetweenButtons = 2.0, - this.paddingOnButtons = 4.0, - this.selectedItem = "", - super.key, - }); - - /// List of items. - final List shops; - - /// Selected item. - final String selectedItem; - - /// Width of the widget. - final double width; - - /// Padding between the buttons. - final double paddingBetweenButtons; - - /// Padding on the buttons. - final double paddingOnButtons; - - /// Callback when an item is tapped. - final Function(Shop shop) onTap; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Wrap( - alignment: WrapAlignment.center, - spacing: 4, - children: [ - for (var shop in shops) ...[ - Padding( - padding: EdgeInsets.only(top: paddingBetweenButtons), - child: InkWell( - onTap: () => onTap(shop), - child: DecoratedBox( - decoration: BoxDecoration( - color: shop.id == selectedItem - ? Theme.of(context).colorScheme.primary - : Colors.white, - borderRadius: BorderRadius.circular(4), - border: Border.all( - color: Theme.of(context).colorScheme.primary, - width: 1, - ), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - shop.name, - style: shop.id == selectedItem - ? theme.textTheme.titleMedium - ?.copyWith(color: Colors.white) - : theme.textTheme.bodyMedium, - ), - ), - ), - ), - ), - ], - ], - ); - } -} diff --git a/packages/flutter_product_page/lib/src/widgets/weekly_discount.dart b/packages/flutter_product_page/lib/src/widgets/weekly_discount.dart deleted file mode 100644 index a22a02c..0000000 --- a/packages/flutter_product_page/lib/src/widgets/weekly_discount.dart +++ /dev/null @@ -1,123 +0,0 @@ -import "package:cached_network_image/cached_network_image.dart"; -import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// A widget that displays a weekly discount. -class WeeklyDiscount extends StatelessWidget { - /// Creates a weekly discount. - const WeeklyDiscount({ - required this.configuration, - required this.product, - super.key, - }); - - /// Configuration for the product page. - final ProductPageConfiguration configuration; - - /// The product for which the discount is displayed. - final Product product; - - /// The top padding of the widget. - static const double topPadding = 20; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - var bottomText = Padding( - padding: const EdgeInsets.all(20.0), - child: Text( - configuration.discountDescription!(product), - style: theme.textTheme.bodyMedium, - textAlign: TextAlign.left, - ), - ); - - var loadingImage = const Padding( - padding: EdgeInsets.all(32.0), - child: Center( - child: CircularProgressIndicator.adaptive(), - ), - ); - - var errorImage = Padding( - padding: const EdgeInsets.all(32.0), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - Icons.error_outline_rounded, - color: Colors.red, - ), - Text(configuration.translations.failedToLoadImageExplenation), - ], - ), - ), - ); - - var image = Padding( - padding: const EdgeInsets.symmetric(horizontal: 1.0), - child: AspectRatio( - aspectRatio: 1, - child: CachedNetworkImage( - imageUrl: product.imageUrl, - width: double.infinity, - fit: BoxFit.cover, - placeholder: (context, url) => loadingImage, - errorWidget: (context, url, error) => errorImage, - ), - ), - ); - - var topText = DecoratedBox( - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.only( - topLeft: Radius.circular(4), - topRight: Radius.circular(4), - ), - ), - child: SizedBox( - width: double.infinity, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, - ), - child: Text( - configuration.translations.discountTitle, - style: theme.textTheme.headlineSmall, - textAlign: TextAlign.left, - ), - ), - ), - ); - - var boxDecoration = BoxDecoration( - border: Border.all( - width: 1.0, - ), - borderRadius: BorderRadius.circular(4.0), - ); - - return Padding( - padding: const EdgeInsets.only(top: topPadding), - child: DecoratedBox( - decoration: boxDecoration, - child: SizedBox( - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - topText, - image, - bottomText, - ], - ), - ), - ), - ); - } -} diff --git a/packages/flutter_product_page/pubspec.yaml b/packages/flutter_product_page/pubspec.yaml deleted file mode 100644 index c28c746..0000000 --- a/packages/flutter_product_page/pubspec.yaml +++ /dev/null @@ -1,35 +0,0 @@ -name: flutter_product_page -description: "A Flutter module for the product page" -publish_to: "none" -version: 2.0.0 - -environment: - sdk: ">=3.3.4 <4.0.0" - -dependencies: - flutter: - sdk: flutter - skeletonizer: ^1.1.1 - cached_network_image: ^3.3.1 - flutter_nested_categories: - git: - url: https://github.com/Iconica-Development/flutter_nested_categories - ref: 0.0.1 - flutter_shopping_interface: - git: - url: https://github.com/Iconica-Development/flutter_shopping - path: packages/flutter_shopping_interface - ref: 2.0.0 - collection: ^1.18.0 - provider: ^6.1.2 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 - -flutter: - uses-material-design: true diff --git a/packages/flutter_shopping/.gitignore b/packages/flutter_shopping/.gitignore index 6c0ee6d..ac5aa98 100644 --- a/packages/flutter_shopping/.gitignore +++ b/packages/flutter_shopping/.gitignore @@ -9,7 +9,6 @@ .history .svn/ migrate_working_dir/ -.metadata # IntelliJ related *.iml @@ -20,37 +19,11 @@ migrate_working_dir/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -.vscode/ +#.vscode/ # Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock **/doc/api/ -**/ios/Flutter/.last_build_id .dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ -pubspec.lock - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release - -# env -*dotenv - -android/ -ios/ -web/ -macos/ -windows/ -linux/ \ No newline at end of file +build/ diff --git a/packages/flutter_shopping/README.md b/packages/flutter_shopping/README.md index abe92eb..4a260d8 100644 --- a/packages/flutter_shopping/README.md +++ b/packages/flutter_shopping/README.md @@ -1,181 +1,39 @@ -# flutter_shopping + -(1) Set up your `MyShop` model by extending from the `ProductPageShop` class. The most basic version looks like this: +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. -```dart -class MyShop extends ProductPageShop { - const MyShop({ - required super.id, - required super.name, - }); -} -``` +## Features -(2) Set up your `MyProduct` model by extending from `ShoppingCartProduct` and extending from the mixin `ProductPageProduct`, like this: +TODO: List what your package can do. Maybe include images, gifs, or videos. -```dart -class MyProduct extends ShoppingCartProduct with ProductPageProduct { - MyProduct({ - required super.id, - required super.name, - required super.price, - required this.category, - required this.imageUrl, - this.discountPrice, - this.hasDiscount = false, - }); +## Getting started - @override - final String category; - - @override - final String imageUrl; - - @override - final double? discountPrice; - - @override - final bool hasDiscount; -} -``` - -(3) Finally in your `routes.dart` import all the routes from the user-story: - -```dart -...getShoppingStoryRoutes( - configuration: ... -), -``` - -(4) Create a new instantiation of the ProductService class: - -```dart -final ProductService productService = ProductService([]); -``` - -(5) Set up the `FlutterShoppingConfiguration`: - -```dart -FlutterShoppingConfiguration( - // (REQUIRED): Shop builder configuration - shopBuilder: (BuildContext context) => ProductPageScreen( - configuration: ProductPageConfiguration( - // (REQUIRED): List of shops that should be displayed - // If there is only one, make a list with just one shop. - shops: getShops(), - - // (REQUIRED): Function to add a product to the cart - onAddToCart: (ProductPageProduct product) => - productService.addProduct(product as MyProduct), - - // (REQUIRED): Function to get the products for a shop - getProducts: (String shopId) => Future.value( - getShopContent(shopId), - ), - - // (REQUIRED): Function to navigate to the shopping cart - onNavigateToShoppingCart: () => onCompleteProductPage(context), - - // (RECOMMENDED) Function that returns the description for a - // product that is on sale. - getDiscountDescription: (ProductPageProduct product) => - """${product.name} for just \$${product.discountPrice?.toStringAsFixed(2)}""", - - // (RECOMMENDED) Function that is fired when the shop selection - // changes. You could use this to clear your shopping cart or to - // change the products so they belong to the correct shop again. - onShopSelectionChange: (ProductPageShop shop) => productService.clear(), - - // (RECOMMENDED) The shop that is initially selected. - // Must be one of the shops in the [shops] list. - initialShop: getShops().first, - - // (RECOMMENDED) Localizations for the product page. - localizations: const ProductPageLocalization(), - - // (OPTIONAL) Appbar - appBar: ... - ), - ), - - // (REQUIRED): Shopping cart builder configuration - shoppingCartBuilder: (BuildContext context) => ShoppingCartScreen( - configuration: ShoppingCartConfig( - // (REQUIRED) product service instance: - productService: productService, - - // (REQUIRED) product item builder: - productItemBuilder: (context, locale, product) => ... - - // (OPTIONAL/REQUIRED) on confirm order callback: - // Either use this callback or the placeOrderButtonBuilder. - onConfirmOrder: (products) => onCompleteShoppingCart(context), - - // (RECOMMENDED) localizations: - localizations: const ShoppingCartLocalizations(), - - // (OPTIONAL) custom appbar: - appBar: ... - ), - ), - - // (REQUIRED): Configuration on what to do when the user story is - // completed. - onCompleteUserStory: (BuildContext context) { - context.go(homePage); - }, - - // (RECOMMENDED) Handle processing of the order details. This function - // should return true if the order was processed successfully, otherwise - // false. - // - // If this function is not provided, it is assumed that the order is - // always processed successfully. - // - // Example use cases that could be implemented here: - // - Sending and storing the order on a server, - // - Processing payment (if the user decides to pay upfront). - // - And many more... - onCompleteOrderDetails: - (BuildContext context, OrderResult orderDetails) async { - ... - }, -) -``` - -For more information about the component specific items please take a look at their repositories: - -- [flutter_product_page](https://github.com/Iconica-Development/flutter_product_page/) -- [flutter_shopping_cart](https://github.com/Iconica-Development/flutter_shopping_cart) -- [flutter_order_details](https://github.com/Iconica-Development/flutter_order_details) +TODO: List prerequisites and provide or point to information on how to +start using the package. ## Usage -For a detailed example you can see the [example](https://github.com/Iconica-Development/flutter_shopping/tree/main/example). +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. -Or, you could run the example yourself: -``` -git clone https://github.com/Iconica-Development/flutter_shopping.git - -cd flutter_shopping - -cd example - -flutter run +```dart +const like = 'sample'; ``` -## Issues +## Additional information -Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_shopping) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl). - -## Want to contribute - -If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_shopping/pulls). - -## Author - -This flutter_shopping for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/flutter_shopping/assets/placeholder.jpg b/packages/flutter_shopping/assets/placeholder.jpg new file mode 100644 index 0000000..6730e3c Binary files /dev/null and b/packages/flutter_shopping/assets/placeholder.jpg differ diff --git a/packages/flutter_order_details/.gitignore b/packages/flutter_shopping/example/.gitignore similarity index 92% rename from packages/flutter_order_details/.gitignore rename to packages/flutter_shopping/example/.gitignore index e31020f..29a3a50 100644 --- a/packages/flutter_order_details/.gitignore +++ b/packages/flutter_shopping/example/.gitignore @@ -9,7 +9,6 @@ .history .svn/ migrate_working_dir/ -.metadata # IntelliJ related *.iml @@ -20,7 +19,7 @@ migrate_working_dir/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -.vscode/ +#.vscode/ # Flutter/Dart/Pub related **/doc/api/ @@ -28,11 +27,9 @@ migrate_working_dir/ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies -.packages .pub-cache/ .pub/ /build/ -pubspec.lock # Symbolication related app.*.symbols @@ -44,6 +41,3 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release - -# env -*dotenv diff --git a/packages/flutter_shopping/example/analysis_options.yaml b/packages/flutter_shopping/example/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/packages/flutter_shopping/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/flutter_shopping/example/fonts/Avenir-Regular.ttf b/packages/flutter_shopping/example/fonts/Avenir-Regular.ttf new file mode 100644 index 0000000..7463844 Binary files /dev/null and b/packages/flutter_shopping/example/fonts/Avenir-Regular.ttf differ diff --git a/packages/flutter_shopping/example/fonts/Merriweather-Regular.ttf b/packages/flutter_shopping/example/fonts/Merriweather-Regular.ttf new file mode 100644 index 0000000..3fecc77 Binary files /dev/null and b/packages/flutter_shopping/example/fonts/Merriweather-Regular.ttf differ diff --git a/packages/flutter_shopping/example/lib/main.dart b/packages/flutter_shopping/example/lib/main.dart new file mode 100644 index 0000000..dd61d4f --- /dev/null +++ b/packages/flutter_shopping/example/lib/main.dart @@ -0,0 +1,30 @@ +import 'package:example/theme.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_shopping/flutter_shopping.dart'; + +void main(List args) { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: theme, + home: const Home(), + ); + } +} + +class Home extends StatelessWidget { + const Home({super.key}); + + @override + Widget build(BuildContext context) { + return const FlutterShoppingNavigatorUserstory( + options: FlutterShoppingOptions(), + ); + } +} diff --git a/packages/flutter_shopping/example/lib/theme.dart b/packages/flutter_shopping/example/lib/theme.dart new file mode 100644 index 0000000..9931a67 --- /dev/null +++ b/packages/flutter_shopping/example/lib/theme.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; + +const Color primaryColor = Color(0xFF71C6D1); + +ThemeData theme = ThemeData( + actionIconTheme: ActionIconThemeData( + backButtonIconBuilder: (context) { + return const Icon(Icons.arrow_back_ios_new_rounded); + }, + ), + scaffoldBackgroundColor: const Color(0xFFFAF9F6), + primaryColor: primaryColor, + checkboxTheme: CheckboxThemeData( + side: const BorderSide( + color: Color(0xFF8D8D8D), + width: 1, + ), + fillColor: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.selected)) { + return primaryColor; + } + return const Color(0xFFEEEEEE); + }, + ), + ), + switchTheme: SwitchThemeData( + trackColor: + WidgetStateProperty.resolveWith((Set states) { + if (!states.contains(WidgetState.selected)) { + return const Color(0xFF8D8D8D); + } + return primaryColor; + }), + thumbColor: const WidgetStatePropertyAll( + Colors.white, + ), + ), + appBarTheme: const AppBarTheme( + centerTitle: true, + iconTheme: IconThemeData( + color: Colors.white, + size: 16, + ), + elevation: 0, + backgroundColor: Color(0xFF212121), + titleTextStyle: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 24, + color: Color(0xFF71C6D1), + fontFamily: "Merriweather", + ), + actionsIconTheme: IconThemeData()), + fontFamily: "Merriweather", + useMaterial3: false, + textTheme: const TextTheme( + headlineSmall: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 16, + color: Colors.white, + ), + headlineLarge: TextStyle( + fontWeight: FontWeight.w900, + fontSize: 24, + color: Color(0xFF71C6D1), + ), + + displayLarge: TextStyle( + fontFamily: "Avenir", + fontWeight: FontWeight.w800, + fontSize: 20, + color: Colors.white, + ), + displayMedium: TextStyle( + fontFamily: "Avenir", + fontWeight: FontWeight.w800, + fontSize: 18, + color: Color(0xFF71C6D1), + ), + displaySmall: TextStyle( + fontFamily: "Avenir", + fontWeight: FontWeight.w800, + fontSize: 14, + color: Colors.black, + ), + + // TITLE + titleSmall: TextStyle( + fontFamily: "Avenir", + fontWeight: FontWeight.w800, + fontSize: 14, + color: Colors.white, + ), + titleMedium: TextStyle( + fontFamily: "Avenir", + fontWeight: FontWeight.w800, + fontSize: 16, + color: Colors.black, + ), + titleLarge: TextStyle( + fontFamily: "Avenir", + fontWeight: FontWeight.w800, + fontSize: 20, + color: Colors.black, + ), + + // LABEL + labelSmall: TextStyle( + fontFamily: "Avenir", + fontWeight: FontWeight.w400, + fontSize: 12, + color: Color(0xFF8D8D8D), + ), + + // BODY + bodySmall: TextStyle( + fontFamily: "Avenir", + fontWeight: FontWeight.w400, + fontSize: 14, + color: Colors.black, + ), + bodyMedium: TextStyle( + fontFamily: "Avenir", + fontWeight: FontWeight.w400, + fontSize: 16, + color: Colors.black, + ), + ), + radioTheme: RadioThemeData( + visualDensity: const VisualDensity( + horizontal: 0, + vertical: -2, + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + fillColor: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.selected)) { + return primaryColor; + } + return Colors.black; + }, + ), + ), + colorScheme: const ColorScheme.light( + primary: primaryColor, + ), +); diff --git a/packages/flutter_shopping/example/pubspec.yaml b/packages/flutter_shopping/example/pubspec.yaml new file mode 100644 index 0000000..269a5ff --- /dev/null +++ b/packages/flutter_shopping/example/pubspec.yaml @@ -0,0 +1,34 @@ +name: example +description: "A new Flutter project." + +publish_to: "none" + +version: 1.0.0+1 + +environment: + sdk: ^3.5.1 + +dependencies: + flutter: + sdk: flutter + flutter_shopping: + path: ../ + shopping_repository_interface: + path: ../../shopping_repository_interface + + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^4.0.0 + +flutter: + uses-material-design: true + fonts: + - family: Merriweather + fonts: + - asset: fonts/Merriweather-Regular.ttf + - family: Avenir + fonts: + - asset: fonts/Avenir-Regular.ttf diff --git a/packages/flutter_shopping/lib/flutter_shopping.dart b/packages/flutter_shopping/lib/flutter_shopping.dart index 7732379..58d91dd 100644 --- a/packages/flutter_shopping/lib/flutter_shopping.dart +++ b/packages/flutter_shopping/lib/flutter_shopping.dart @@ -1,9 +1,30 @@ -/// Flutter Shopping -library flutter_shopping; +/// userstory +// ignore_for_file: directives_ordering -export "package:flutter_order_details/flutter_order_details.dart"; -export "package:flutter_product_page/flutter_product_page.dart"; -export "package:flutter_shopping_cart/flutter_shopping_cart.dart"; +library; -export "src/configuration/shopping_configuration.dart"; +export "package:shopping_repository_interface/shopping_repository_interface.dart"; export "src/flutter_shopping_navigator_userstory.dart"; + +/// screens +export "src/screens/shopping_screen.dart"; +export "src/screens/shopping_cart_screen.dart"; +export "src/screens/filter_screen.dart"; +export "src/screens/personal_information_screen.dart"; +export "src/screens/date_time_information_screen.dart"; +export "src/screens/payment_options_screen.dart"; + +/// models +export "src/models/shopping_translations.dart"; +export "src/models/flutter_shopping_options.dart"; + +/// widgets +export "src/widgets/primary_button.dart"; +export "src/widgets/shop_selector.dart"; +export "src/widgets/filter_item.dart"; +export "src/widgets/shopping_cart_item.dart"; +export "src/widgets/info_bottomsheet.dart"; +export "src/widgets/product_item_list.dart"; +export "src/widgets/product_item.dart"; +export "src/widgets/shop_selector_item.dart"; +export "src/widgets/weekly_offer.dart"; diff --git a/packages/flutter_shopping/lib/src/configuration/shopping_configuration.dart b/packages/flutter_shopping/lib/src/configuration/shopping_configuration.dart deleted file mode 100644 index 2cb20bb..0000000 --- a/packages/flutter_shopping/lib/src/configuration/shopping_configuration.dart +++ /dev/null @@ -1,240 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// configuration for the shopping userstory -class ShoppingConfiguration { - /// constructor for the userstory configuration - const ShoppingConfiguration({ - /// ProductPage configurations - required this.shoppingService, - this.onGetProducts, - this.onGetShops, - this.onAddToCart, - this.onNavigateToShoppingCart, - this.getProductsInShoppingCart, - this.shoppingCartButtonBuilder, - this.productBuilder, - this.onShopSelectionChange, - this.productPageTranslations, - this.shopSelectorStyle, - this.productPagePagePadding, - this.productPageAppBarBuilder, - this.bottomNavigationBarBuilder, - this.onProductDetail, - this.discountDescription, - this.noContentBuilder, - this.errorBuilder, - this.categoryListBuilder, - this.shopselectorBuilder, - this.discountBuilder, - this.selectedCategoryBuilder, - - /// ShoppingCart configurations - this.onConfirmOrder, - this.productItemBuilder, - this.confirmOrderButtonBuilder, - this.confirmOrderButtonHeight, - this.sumBottomSheetBuilder, - this.sumBottomSheetHeight, - this.titleBuilder, - this.shoppingCartTranslations, - this.shoppingCartPagePadding, - this.shoppingCartBottomPadding, - this.shoppingCartAppBarBuilder, - - /// OrderDetail configurations - this.onNextStep, - this.onStepsCompleted, - this.onCompleteOrderDetails, - this.pages, - this.orderDetailTranslations, - this.orderDetailAppBarBuilder, - this.orderDetailNextbuttonBuilder, - this.orderSuccessBuilder, - }); - - /// The service that will be used for the userstory - final ShoppingService shoppingService; - - /// Builder for the list of selected categories - final Widget Function(ProductPageConfiguration configuration)? - selectedCategoryBuilder; - - /// Function that will be called when the products are requested - final Future> Function(String shopId)? onGetProducts; - - /// Function that will be called when the shops are requested - final Future> Function()? onGetShops; - - /// Function that will be called when an item is added to the shopping cart - final Function(Product)? onAddToCart; - - /// Function that will be called when the user navigates to the shopping cart - final Function()? onNavigateToShoppingCart; - - /// Function that will be called to get the amount of - /// products in the shopping cart - final int Function()? getProductsInShoppingCart; - - /// Default shopping cart button builder - final Widget Function(BuildContext, ProductPageConfiguration)? - shoppingCartButtonBuilder; - - /// ProductPage item builder - final Widget Function( - BuildContext, - Product, - ProductPageConfiguration configuration, - )? productBuilder; - - /// Function that will be called when the shop selection changes - final Function(Shop)? onShopSelectionChange; - - /// Translations for the product page - final ProductPageTranslations? productPageTranslations; - - /// Shop selector style - final ShopSelectorStyle? shopSelectorStyle; - - /// ProductPage padding - final EdgeInsets? productPagePagePadding; - - /// AppBar builder - final AppBar Function(BuildContext)? productPageAppBarBuilder; - - /// BottomNavigationBarBuilder - final Widget? bottomNavigationBarBuilder; - - /// Function that will be called when the product detail is requested - final Function(BuildContext, Product, String)? onProductDetail; - - /// Function that will be called when the discount description is requested - final String Function(Product)? discountDescription; - - /// Function that will be called when there are no products - final Widget Function(BuildContext)? noContentBuilder; - - /// Function that will be called when there is an error - final Widget Function(BuildContext, Object?)? errorBuilder; - - /// Builder for the shop selector. This builder is used to build the shop - /// selector that will be displayed in the product page. - final Widget Function( - BuildContext context, - ProductPageConfiguration configuration, - List shops, - Function(Shop shop) onShopSelectionChange, - )? shopselectorBuilder; - - /// Builder for the discount widget. This builder is used to build the - /// discount widget that will be displayed in the product page. - final Widget Function( - BuildContext context, - ProductPageConfiguration configuration, - List discountedProducts, - )? discountBuilder; - - /// Builder for the list of items that are displayed in the product page. - final Widget Function( - BuildContext context, - ProductPageConfiguration configuration, - List products, - )? categoryListBuilder; - - /// Function that will be called when the order button on - /// the shopping cart page is pressed - final Function(List)? onConfirmOrder; - - /// Shopping cart item builder - final Widget Function(BuildContext, Product, ShoppingCartConfig)? - productItemBuilder; - - /// Shopping cart confirm order button builder - final Widget Function( - BuildContext, - ShoppingCartConfig, - dynamic Function(List), - )? confirmOrderButtonBuilder; - - /// The height of the confirm order button - /// This will not set the height of the button itself - /// this is only used to create some extra space on the bottom - /// of the product list so the button doesn't overlap with the - /// last product - final double? confirmOrderButtonHeight; - - /// Shopping cart sum bottom sheet builder - final Widget Function(BuildContext, ShoppingCartConfig)? - sumBottomSheetBuilder; - - /// The height of the sum bottom sheet - /// This will not set the height of the sheet itself - /// this is only used to create some extra space on the bottom - /// of the product list so the sheet doesn't overlap with the - /// last product - final double? sumBottomSheetHeight; - - /// Function to override the title on the shopping cart screen - final Widget Function(BuildContext, String)? titleBuilder; - - /// Shopping cart translations - final ShoppingCartTranslations? shoppingCartTranslations; - - /// Shopping cart page padding - final EdgeInsets? shoppingCartPagePadding; - - /// Shopping cart bottom padding - final EdgeInsets? shoppingCartBottomPadding; - - /// Shopping cart app bar builder - final AppBar Function(BuildContext)? shoppingCartAppBarBuilder; - - /// Function that gets called when the user navigates to the next - /// step of the order details - final dynamic Function( - int, - Map, - FlutterFormController controller, - )? onNextStep; - - /// Function that gets called when the Navigates - /// to the order confirmationp page - final dynamic Function( - String, - List, - Map>, - OrderDetailConfiguration, - )? onStepsCompleted; - - /// Function that gets called when pressing the complete order - /// button on the confirmation page - final Function(BuildContext, OrderDetailConfiguration)? - onCompleteOrderDetails; - - /// The order detail pages that are used in the order detail screen - final List Function(BuildContext)? pages; - - /// The translations for the order detail screen - final OrderDetailTranslations? orderDetailTranslations; - - /// The app bar for the order detail screen - final AppBar Function(BuildContext, String)? orderDetailAppBarBuilder; - - /// The builder for the next button on the order detail screen - final Widget Function( - int, - // ignore: avoid_positional_boolean_parameters - bool, - BuildContext, - OrderDetailConfiguration, - FlutterFormController, - )? orderDetailNextbuttonBuilder; - - /// The builder for the order success screen - final Widget? Function( - BuildContext, - OrderDetailConfiguration, - Map>, - )? orderSuccessBuilder; -} diff --git a/packages/flutter_shopping/lib/src/flutter_shopping_navigator_userstory.dart b/packages/flutter_shopping/lib/src/flutter_shopping_navigator_userstory.dart index b19bbb7..75e5980 100644 --- a/packages/flutter_shopping/lib/src/flutter_shopping_navigator_userstory.dart +++ b/packages/flutter_shopping/lib/src/flutter_shopping_navigator_userstory.dart @@ -1,235 +1,109 @@ import "package:flutter/material.dart"; import "package:flutter_shopping/flutter_shopping.dart"; -import "package:flutter_shopping_local/flutter_shopping_local.dart"; -/// User story for the shopping navigator. -class ShoppingNavigatorUserStory extends StatelessWidget { - /// Constructor for the shopping navigator user story. - const ShoppingNavigatorUserStory({ - this.shoppingConfiguration, +/// A userstory for the FlutterShoppingNavigator. +class FlutterShoppingNavigatorUserstory extends StatefulWidget { + /// Constructor for the FlutterShoppingNavigatorUserstory. + const FlutterShoppingNavigatorUserstory({ + this.options = const FlutterShoppingOptions(), + this.translations = const ShoppingTranslations(), + this.shoppingService, + this.initialShopId, super.key, }); - /// Shopping configuration. - final ShoppingConfiguration? shoppingConfiguration; + /// The options for the shopping navigator. + final FlutterShoppingOptions options; + + /// The shopping service. + final ShoppingService? shoppingService; + + /// The translations. + final ShoppingTranslations translations; + + /// The initial shop id. + final String? initialShopId; @override - Widget build(BuildContext context) => ShoppingProductPage( - shoppingConfiguration: shoppingConfiguration ?? - ShoppingConfiguration( - shoppingService: LocalShoppingService(), - ), - ); + State createState() => + _FlutterShoppingNavigatorUserstoryState(); } -/// Shopping product page. -class ShoppingProductPage extends StatelessWidget { - /// Constructor for the shopping product page. - const ShoppingProductPage({ - required this.shoppingConfiguration, - super.key, - }); - - /// Shopping configuration. - final ShoppingConfiguration shoppingConfiguration; +class _FlutterShoppingNavigatorUserstoryState + extends State { + late ShoppingService shoppingService; @override - Widget build(BuildContext context) { - var service = shoppingConfiguration.shoppingService; - return ProductPageScreen( - configuration: ProductPageConfiguration( - shoppingService: service, - shoppingCartButtonBuilder: - shoppingConfiguration.shoppingCartButtonBuilder, - productBuilder: shoppingConfiguration.productBuilder, - onShopSelectionChange: shoppingConfiguration.onShopSelectionChange, - translations: shoppingConfiguration.productPageTranslations ?? - const ProductPageTranslations(), - shopSelectorStyle: - shoppingConfiguration.shopSelectorStyle ?? ShopSelectorStyle.row, - pagePadding: shoppingConfiguration.productPagePagePadding ?? - const EdgeInsets.all(4), - appBarBuilder: shoppingConfiguration.productPageAppBarBuilder, - bottomNavigationBar: shoppingConfiguration.bottomNavigationBarBuilder, - onProductDetail: shoppingConfiguration.onProductDetail, - discountDescription: shoppingConfiguration.discountDescription, - noContentBuilder: shoppingConfiguration.noContentBuilder, - errorBuilder: shoppingConfiguration.errorBuilder, - shopselectorBuilder: shoppingConfiguration.shopselectorBuilder, - discountBuilder: shoppingConfiguration.discountBuilder, - categoryListBuilder: shoppingConfiguration.categoryListBuilder, - selectedCategoryBuilder: shoppingConfiguration.selectedCategoryBuilder, - shops: () async { - if (shoppingConfiguration.onGetShops != null) { - return shoppingConfiguration.onGetShops!(); - } else { - return service.shopService.getShops(); - } + void initState() { + shoppingService = widget.shoppingService ?? ShoppingService(); + super.initState(); + } + + @override + Widget build(BuildContext context) => shoppingScreen(); + + Widget shoppingScreen() => ShoppingScreen( + initialShopId: widget.initialShopId, + translations: widget.translations, + shoppingService: shoppingService, + options: widget.options, + onFilterPressed: () async { + widget.options.onFilterPressed?.call() ?? await push(filterScreen()); }, - getProducts: (shop) async { - if (shoppingConfiguration.onGetProducts != null) { - return shoppingConfiguration.onGetProducts!(shop.id); - } else { - return service.productService.getProducts(shop.id); - } + onShoppingCartPressed: () async { + widget.options.onCartPressed?.call() ?? + await push(shoppingCartScreen()); }, - onAddToCart: (product) { - if (shoppingConfiguration.onAddToCart != null) { - shoppingConfiguration.onAddToCart!(product); - return; - } else { - return service.shoppingCartService.addProduct(product); - } + ); + + Widget filterScreen() => FilterScreen( + translations: widget.translations, + shoppingService: shoppingService, + options: widget.options, + ); + + Widget shoppingCartScreen() => ShoppingCartScreen( + translations: widget.translations, + shoppingService: shoppingService, + options: widget.options, + onOrder: () async { + widget.options.onOrderPressed?.call() ?? + await push(personalInformationScreen()); }, - onNavigateToShoppingCart: () async { - if (shoppingConfiguration.onNavigateToShoppingCart != null) { - return shoppingConfiguration.onNavigateToShoppingCart!(); - } else { - return Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ShoppingCart( - shoppingConfiguration: shoppingConfiguration, - ), - ), - ); - } + ); + Widget personalInformationScreen() => PersonalInformationScreen( + translations: widget.translations, + options: widget.options, + onFinished: (value) async { + widget.options.getPersonalInformation?.call(value); + widget.options.onPersonalInformationPressed?.call() ?? + await push(dateTimeInformationScreen()); }, - getProductsInShoppingCart: () { - if (shoppingConfiguration.getProductsInShoppingCart != null) { - return shoppingConfiguration.getProductsInShoppingCart!(); - } else { - return service.shoppingCartService.countProducts(); - } + ); + + Widget dateTimeInformationScreen() => DateTimeInformationScreen( + translations: widget.translations, + onFinished: (value) async { + widget.options.getDateTimeInformation?.call(value); + widget.options.onDateTimePressed?.call() ?? + await push(paymentOptionsScreen()); }, - ), - ); + ); + + Widget paymentOptionsScreen() => PaymentOptionsScreen( + translations: widget.translations, + onFinished: (value) async { + widget.options.getPaymentInformation?.call(value); + widget.options.onPaymentPressed?.call() ?? await popUntil(); + }, + ); + + Future push(Widget screen) async { + await Navigator.of(context) + .push(MaterialPageRoute(builder: (context) => screen)); + } + + Future popUntil() async { + Navigator.of(context).popUntil((route) => route.isFirst); } } - -/// Shopping cart. -class ShoppingCart extends StatelessWidget { - /// Constructor for the shopping cart. - const ShoppingCart({ - required this.shoppingConfiguration, - super.key, - }); - - /// Shopping configuration. - final ShoppingConfiguration shoppingConfiguration; - - @override - Widget build(BuildContext context) { - var service = shoppingConfiguration.shoppingService.shoppingCartService; - return ShoppingCartScreen( - configuration: ShoppingCartConfig( - service: service, - productItemBuilder: shoppingConfiguration.productItemBuilder, - confirmOrderButtonBuilder: - shoppingConfiguration.confirmOrderButtonBuilder, - confirmOrderButtonHeight: - shoppingConfiguration.confirmOrderButtonHeight ?? 100, - sumBottomSheetBuilder: shoppingConfiguration.sumBottomSheetBuilder, - sumBottomSheetHeight: shoppingConfiguration.sumBottomSheetHeight ?? 100, - titleBuilder: shoppingConfiguration.titleBuilder, - translations: shoppingConfiguration.shoppingCartTranslations ?? - const ShoppingCartTranslations(), - pagePadding: shoppingConfiguration.shoppingCartPagePadding ?? - const EdgeInsets.symmetric(horizontal: 32), - bottomPadding: shoppingConfiguration.shoppingCartBottomPadding ?? - const EdgeInsets.fromLTRB(44, 0, 44, 32), - appBarBuilder: shoppingConfiguration.shoppingCartAppBarBuilder, - onConfirmOrder: (products) async { - if (shoppingConfiguration.onConfirmOrder != null) { - return shoppingConfiguration.onConfirmOrder!(products); - } else { - return Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => ShoppingOrderDetails( - shoppingConfiguration: shoppingConfiguration, - ), - ), - ); - } - }, - ), - ); - } -} - -/// Shopping order details. -class ShoppingOrderDetails extends StatelessWidget { - /// Constructor for the shopping order details. - const ShoppingOrderDetails({ - required this.shoppingConfiguration, - super.key, - }); - - /// Shopping configuration. - final ShoppingConfiguration shoppingConfiguration; - - @override - Widget build(BuildContext context) => OrderDetailScreen( - configuration: OrderDetailConfiguration( - shoppingService: shoppingConfiguration.shoppingService, - pages: shoppingConfiguration.pages, - translations: shoppingConfiguration.orderDetailTranslations ?? - const OrderDetailTranslations(), - appBarBuilder: shoppingConfiguration.orderDetailAppBarBuilder, - nextbuttonBuilder: shoppingConfiguration.orderDetailNextbuttonBuilder, - orderSuccessBuilder: (context, configuration, data) => - shoppingConfiguration.orderSuccessBuilder - ?.call(context, configuration, data) ?? - DefaultOrderSucces( - configuration: configuration, - orderDetails: data, - ), - onNextStep: (currentStep, data, controller) async { - if (shoppingConfiguration.onNextStep != null) { - return shoppingConfiguration.onNextStep!( - currentStep, - data, - controller, - ); - } else { - await controller.autoNextStep(); - } - }, - onStepsCompleted: (shopId, products, data, configuration) async { - if (shoppingConfiguration.onStepsCompleted != null) { - return shoppingConfiguration.onStepsCompleted!( - shopId, - products, - data, - configuration, - ); - } else { - return Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => DefaultOrderSucces( - configuration: configuration, - orderDetails: data, - ), - ), - ); - } - }, - onCompleteOrderDetails: (context, configuration) async { - if (shoppingConfiguration.onCompleteOrderDetails != null) { - return shoppingConfiguration.onCompleteOrderDetails!( - context, - configuration, - ); - } else { - shoppingConfiguration.shoppingService.shoppingCartService.clear(); - return Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (context) => ShoppingProductPage( - shoppingConfiguration: shoppingConfiguration, - ), - ), - ); - } - }, - ), - ); -} diff --git a/packages/flutter_shopping/lib/src/models/flutter_shopping_options.dart b/packages/flutter_shopping/lib/src/models/flutter_shopping_options.dart new file mode 100644 index 0000000..108b80b --- /dev/null +++ b/packages/flutter_shopping/lib/src/models/flutter_shopping_options.dart @@ -0,0 +1,132 @@ +import "package:flutter/material.dart"; +import "package:shopping_repository_interface/shopping_repository_interface.dart"; + +/// The options for the flutter shopping package. +class FlutterShoppingOptions { + /// The constructor for the FlutterShoppingOptions. + const FlutterShoppingOptions({ + this.shoppingScreenAppbarBuilder, + this.filterChipbuilder, + this.weeklyOfferBuilder, + this.productItemBuilder, + this.primaryButtonBuilder, + this.shopSelectorItemBuilder, + this.filterScreenAppbarBuilder, + this.filterItemBuilder, + this.shoppingCartItemBuilder, + this.onInfoPressed, + this.informationInputDecoration, + this.onFilterPressed, + this.onCartPressed, + this.onOrderPressed, + this.onPersonalInformationPressed, + this.onDateTimePressed, + this.onPaymentPressed, + this.shoppingScreenDrawer, + this.getPersonalInformation, + this.getDateTimeInformation, + this.getPaymentInformation, + }); + + /// The appbar builder for the shopping screen. + final AppBarBuilder? shoppingScreenAppbarBuilder; + + /// The weekly offer builder for the shopping screen. + final ItemBuilder? weeklyOfferBuilder; + + /// The product item builder for the shopping screen. + final ItemBuilder? productItemBuilder; + + /// The filter chip builder for the shopping screen. + final Widget? Function( + BuildContext context, + String title, + Function() onDelete, + )? filterChipbuilder; + + /// The primary button builder. + final Widget Function( + BuildContext context, + // ignore: avoid_positional_boolean_parameters + bool enabled, + String title, + Function() onPressed, + )? primaryButtonBuilder; + + /// shop selector item builder. + final Widget Function( + BuildContext context, + Shop shop, + Function(Shop) onSelected, + )? shopSelectorItemBuilder; + + /// The appbar builder for the filter screen. + final AppBarBuilder? filterScreenAppbarBuilder; + + /// The filter item builder. + final Widget Function( + BuildContext context, + Function() onDeselect, + Function() onSelect, + // ignore: avoid_positional_boolean_parameters + bool currentValue, + )? filterItemBuilder; + + /// The shopping cart item builder. + final Widget Function( + BuildContext context, + Product product, + Function() onAddToCart, + Function() onRemoveFromCart, + )? shoppingCartItemBuilder; + + /// The on info pressed function. + final Function(Product product)? onInfoPressed; + + /// The input decoration for the information. + final InputDecoration? Function(BuildContext context, String hintText)? + informationInputDecoration; + + /// on filter pressed. + final Function()? onFilterPressed; + + /// on cart pressed. + final Function()? onCartPressed; + + /// on order pressed. + final Function()? onOrderPressed; + + /// on personal information pressed. + final Function()? onPersonalInformationPressed; + + /// on address pressed. + final Function()? onDateTimePressed; + + /// on payment pressed. + final Function()? onPaymentPressed; + + /// The shopping screen drawer. + final Widget? shoppingScreenDrawer; + + /// The function to get the personal information. + final Function(Map value)? getPersonalInformation; + + /// The function to get the address information. + final Function(Map value)? getDateTimeInformation; + + /// The function to get the payment information. + final Function(Map value)? getPaymentInformation; +} + +/// The appbar builder. +typedef AppBarBuilder = AppBar Function( + BuildContext context, + String title, + Function()? onFilterPressed, +); + +/// The item builder. +typedef ItemBuilder = Widget Function( + BuildContext context, + Product product, +); diff --git a/packages/flutter_shopping/lib/src/models/shopping_translations.dart b/packages/flutter_shopping/lib/src/models/shopping_translations.dart new file mode 100644 index 0000000..4e895fb --- /dev/null +++ b/packages/flutter_shopping/lib/src/models/shopping_translations.dart @@ -0,0 +1,225 @@ +/// Translations for the Shopping package. +class ShoppingTranslations { + /// Constructor for the Shopping translations. + const ShoppingTranslations({ + /// screen titles + this.filterTitle = "filter", + this.shoppingCartTitle = "Shopping cart", + this.personalInformationTitle = "information", + this.dateTimeInformationTitle = "information", + this.paymentOptionsTitle = "information", + + /// button text + this.orderButton = "Order", + this.closeInfo = "Close", + this.viewShoppingCart = "View shopping cart", + this.personalInformationButton = "Choose date and time", + this.paymentButton = "Next", + this.datetimeInformationButton = "Next", + + /// body text + this.weeklyOffer = "Weekly offer", + this.whatWouldyouLikeToOrder = "What would you like to order?", + this.shoppingCartProducts = "Products", + this.shoppingCartTotal = "Subtotal", + this.shoppingCartCurrency = "€", + this.personalInformationName = "What’s your name?", + this.personalInformationAddress = "What’s your address?", + this.personalInformationPhone = "What’s your phone number?", + this.personalInformationEmail = "What’s your email address?", + this.personalInformationComment = "Do you have any comments?", + this.paymenttitle = "Payment method", + this.paymentExplainer = + "Choose when you would like to to pay for the order.", + this.payNow = "Pay now", + this.payLater = "Pay later", + this.chooseDateAndTime = + "When and at what time would you like to pick up your order?", + this.selectDay = "Select a day", + this.dayToday = "Today", + this.dayTomorrow = "Tomorrow", + this.morning = "Morning", + this.afternoon = "Afternoon", + this.selectTime = "Select a time", + + /// error messages + this.nameRequired = "Name is required", + this.addressRequired = "Please enter a street and house number", + this.postalCodeRequired = "Please enter your postal code", + this.cityRequired = "Please enter your city", + this.phoneRequired = "Please enter your phone number", + this.emailRequired = "Email is required", + this.invalidEmail = "Please fill in a valid email address", + this.paymentError = "Please select a payment method", + this.invalidAdress = "Invalid street and house number", + this.invalidPostalCode = "Invalid postal code", + this.invalidPhoneLength = "Invalid phone number length", + this.phoneContainsLettersError = "Phone number can only contain digits", + this.selectDayError = "Please select a day", + + /// hint text + this.nameHint = "full name", + this.addressHint = "street name and number", + this.postalCodeHint = "postal code", + this.cityHint = "city", + this.phoneHint = "phone number", + this.emailHint = "email address", + this.commentHint = "optional", + }); + + /// Appbar title for filter screen. + final String filterTitle; + + /// Appbar title for shopping cart screen. + final String shoppingCartTitle; + + /// Appbar title for personal information screen. + final String personalInformationTitle; + + /// Text for the order button. + final String orderButton; + + /// Text for the close info button. + final String closeInfo; + + /// Text for the view shopping cart button. + final String viewShoppingCart; + + /// Text for the personal information button. + final String personalInformationButton; + + /// Text for the weekly offer. + final String weeklyOffer; + + /// Text for the question what would you like to order. + final String whatWouldyouLikeToOrder; + + /// Text for the products. + final String shoppingCartProducts; + + /// Text for the total price in the shopping cart. + final String shoppingCartTotal; + + /// The currency that is being used + final String shoppingCartCurrency; + + /// Text for the personal information fields. + final String personalInformationName; + + /// Text for the personal information fields. + final String personalInformationAddress; + + /// Text for the personal information fields. + final String personalInformationPhone; + + /// Text for the personal information fields. + final String personalInformationEmail; + + /// Text for the personal information fields. + final String personalInformationComment; + + /// Text for the name required field. + final String nameRequired; + + /// Text for the address field. + final String addressRequired; + + /// Text for the postal code field. + final String postalCodeRequired; + + /// Text for the city field. + final String cityRequired; + + /// Text for the phone field. + final String phoneRequired; + + /// Text for the email field. + final String emailRequired; + + /// Text for the invalid email field. + final String invalidEmail; + + /// Text for the name hint. + final String nameHint; + + /// Text for the address hint. + final String addressHint; + + /// Text for the postal code hint. + final String postalCodeHint; + + /// Text for the city hint. + final String cityHint; + + /// Text for the phone hint. + final String phoneHint; + + /// Text for the email hint. + final String emailHint; + + /// Text for the comment hint. + final String commentHint; + + /// Text for the date and time information title. + final String dateTimeInformationTitle; + + /// Text for the payment options title. + final String paymentOptionsTitle; + + /// Text for the payment error. + final String paymentError; + + /// Text for the payment title. + final String paymenttitle; + + /// Text for the payment explainer. + final String paymentExplainer; + + /// Text for the pay now button. + final String payNow; + + /// Text for the pay later button. + final String payLater; + + /// Text for the payment button. + final String paymentButton; + + /// Text for the invalid address. + final String invalidAdress; + + /// Text for the invalid postal code. + final String invalidPostalCode; + + /// Text for the invalid phone length. + final String invalidPhoneLength; + + /// Text for the phone contains letters error. + final String phoneContainsLettersError; + + /// Text for the select day error. + final String datetimeInformationButton; + + /// Text for the choose date and time. + final String chooseDateAndTime; + + /// Text for the select day. + final String selectDay; + + /// Text for the select day error. + final String selectDayError; + + /// Text for the day today. + final String dayToday; + + /// Text for the day tomorrow. + final String dayTomorrow; + + /// Text for the morning. + final String morning; + + /// Text for the afternoon. + final String afternoon; + + /// Text for the select time. + final String selectTime; +} diff --git a/packages/flutter_shopping/lib/src/screens/date_time_information_screen.dart b/packages/flutter_shopping/lib/src/screens/date_time_information_screen.dart new file mode 100644 index 0000000..29f45ff --- /dev/null +++ b/packages/flutter_shopping/lib/src/screens/date_time_information_screen.dart @@ -0,0 +1,257 @@ +import "package:animated_toggle/animated_toggle.dart"; +import "package:flutter/material.dart"; +import "package:flutter_form_wizard/flutter_form.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; + +/// The morning times. +List morningTimes = [ + "09:00", + "09:15", + "09:30", + "09:45", + "10:00", + "10:15", + "10:30", + "10:45", + "11:00", + "11:15", + "11:30", + "11:45", +]; + +/// The afternoon times. +List afternoonTimes = [ + "12:00", + "12:15", + "12:30", + "12:45", + "13:00", + "13:15", + "13:30", + "13:45", + "14:00", + "14:15", + "14:30", + "14:45", + "15:00", + "15:15", + "15:30", + "15:45", + "16:00", + "16:15", + "16:30", + "16:45", + "17:00", +]; + +/// The personal information page. +class DateTimeInformationScreen extends StatefulWidget { + /// Constructor for the PersonalInformationScreen. + const DateTimeInformationScreen({ + required this.translations, + required this.onFinished, + super.key, + }); + + /// The translations. + final ShoppingTranslations translations; + + /// The on finished function. + final Function(Map) onFinished; + + @override + State createState() => + _DateTimeInformationScreenState(); +} + +class _DateTimeInformationScreenState extends State { + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var dateTimeInformationController = FlutterFormController(); + var multipleChoiceController = FlutterFormInputMultipleChoiceController( + id: "multipleChoice", + mandatory: true, + ); + InputDecoration dropdownInputDecoration(String hint) => InputDecoration( + hintStyle: theme.textTheme.bodySmall, + hintText: hint, + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + ); + + var switchStatus = ValueNotifier(false); + + return Scaffold( + appBar: AppBar( + title: Text(widget.translations.dateTimeInformationTitle), + ), + body: FlutterForm( + formController: dateTimeInformationController, + options: FlutterFormOptions( + pages: [ + FlutterFormPage( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + widget.translations.chooseDateAndTime, + style: theme.textTheme.titleMedium, + ), + ), + const SizedBox( + height: 4, + ), + FlutterFormInputDropdown( + icon: const Icon( + Icons.keyboard_arrow_down, + color: Colors.black, + ), + isDense: true, + decoration: dropdownInputDecoration( + widget.translations.selectDay, + ), + validationMessage: widget.translations.selectDayError, + controller: FlutterFormInputDropdownController( + id: "date", + mandatory: true, + ), + items: [ + DropdownMenuItem( + value: widget.translations.dayToday, + child: Text( + widget.translations.dayToday, + style: theme.textTheme.bodySmall, + ), + ), + DropdownMenuItem( + value: widget.translations.dayTomorrow, + child: Text( + widget.translations.dayTomorrow, + style: theme.textTheme.bodySmall, + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: ValueListenableBuilder( + valueListenable: switchStatus, + builder: (context, value, child) => AnimatedToggle( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + blurRadius: 5, + color: + theme.colorScheme.primary.withOpacity(0.8), + ), + ], + color: Colors.white, + borderRadius: BorderRadius.circular(50), + ), + width: 280, + toggleColor: theme.colorScheme.primary, + onSwitch: (value) { + switchStatus.value = !switchStatus.value; + }, + childLeft: Center( + child: Text( + widget.translations.morning, + style: theme.textTheme.titleSmall?.copyWith( + color: switchStatus.value + ? theme.colorScheme.primary + : Colors.white, + ), + ), + ), + childRight: Center( + child: Text( + widget.translations.afternoon, + style: theme.textTheme.titleSmall?.copyWith( + color: switchStatus.value + ? Colors.white + : theme.colorScheme.primary, + ), + ), + ), + ), + ), + ), + const SizedBox( + height: 8, + ), + ValueListenableBuilder( + valueListenable: switchStatus, + builder: (context, value, child) => + FlutterFormInputMultipleChoice( + validationMessage: widget.translations.selectTime, + controller: multipleChoiceController, + options: + switchStatus.value ? afternoonTimes : morningTimes, + mainAxisSpacing: 5, + crossAxisSpacing: 5, + childAspectRatio: 2, + height: MediaQuery.of(context).size.height * 0.6, + builder: ( + context, + index, + selected, + controller, + options, + state, + ) => + GestureDetector( + onTap: () { + state.didChange(options[index]); + selected.value = index; + controller.onSaved(options[index]); + }, + child: Container( + decoration: BoxDecoration( + color: selected.value == index + ? Theme.of(context).colorScheme.primary + : Colors.white, + borderRadius: BorderRadius.circular(10), + ), + height: 40, + child: Center( + child: Text(options[index]), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ], + nextButton: (pageNumber, checkingPages) => Padding( + padding: const EdgeInsets.only(bottom: 32), + child: Align( + alignment: Alignment.bottomCenter, + child: PrimaryButton( + text: widget.translations.personalInformationButton, + onPressed: () async { + await dateTimeInformationController.autoNextStep(); + }, + ), + ), + ), + onFinished: (values) { + widget.onFinished(values.entries.first.value); + }, + onNext: (page, values) {}, + ), + ), + ); + } +} diff --git a/packages/flutter_shopping/lib/src/screens/filter_screen.dart b/packages/flutter_shopping/lib/src/screens/filter_screen.dart new file mode 100644 index 0000000..f369ec0 --- /dev/null +++ b/packages/flutter_shopping/lib/src/screens/filter_screen.dart @@ -0,0 +1,106 @@ +import "package:flutter/material.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; +import "package:rxdart/rxdart.dart"; + +/// filter screen +class FilterScreen extends StatelessWidget { + /// Constructor for the FilterScreen. + const FilterScreen({ + required this.shoppingService, + required this.translations, + required this.options, + super.key, + }); + + /// The shopping service. + final ShoppingService shoppingService; + + /// The translations. + final ShoppingTranslations translations; + + /// The options. + final FlutterShoppingOptions options; + + @override + Widget build(BuildContext context) => Scaffold( + appBar: options.filterScreenAppbarBuilder?.call( + context, + translations.filterTitle, + () { + Navigator.of(context).pop(); + }, + ) ?? + AppBar( + leading: const SizedBox.shrink(), + title: Text(translations.filterTitle), + actions: [ + IconButton( + onPressed: () { + Navigator.of(context).pop(); + }, + icon: const Icon( + Icons.close_rounded, + size: 28, + ), + ), + ], + ), + body: SingleChildScrollView( + child: StreamBuilder>( + stream: Rx.combineLatest( + [ + shoppingService.getCategories(), + shoppingService.getSelectedCategoryStream(), + ], + (List data) => data, + ), + builder: (context, snapshot) { + if (snapshot.hasData) { + var categories = snapshot.data![0] as List; + var selectedCategories = snapshot.data![1] as List; + + return Column( + children: [ + ...categories.map( + (category) { + var isSelected = containsCategoryById( + selectedCategories, + category.id, + ); + return options.filterItemBuilder?.call( + context, + () { + shoppingService.deselectCategory(category.id); + }, + () { + shoppingService.selectCategory(category.id); + }, + isSelected, + ) ?? + FilterItem( + value: isSelected, + onChanged: (value) { + if (value) { + shoppingService.selectCategory(category.id); + } else { + shoppingService.deselectCategory(category.id); + } + }, + category: category, + ); + }, + ), + ], + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ), + ); +} + +/// Check if the category is selected. +bool containsCategoryById(List categories, String id) => + categories.any((category) => category.id == id); diff --git a/packages/flutter_shopping/lib/src/screens/payment_options_screen.dart b/packages/flutter_shopping/lib/src/screens/payment_options_screen.dart new file mode 100644 index 0000000..0fbc459 --- /dev/null +++ b/packages/flutter_shopping/lib/src/screens/payment_options_screen.dart @@ -0,0 +1,118 @@ +import "package:flutter/material.dart"; +import "package:flutter_form_wizard/flutter_form.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; + +/// A screen for the payment options. +class PaymentOptionsScreen extends StatelessWidget { + /// Constructor for the PaymentOptionsScreen. + const PaymentOptionsScreen({ + required this.translations, + required this.onFinished, + super.key, + }); + + /// The translations. + final ShoppingTranslations translations; + + /// The on finished function. + final Function(Map) onFinished; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var paymentOptionsController = FlutterFormController(); + return Scaffold( + appBar: AppBar( + title: Text(translations.paymentOptionsTitle), + ), + body: FlutterForm( + formController: paymentOptionsController, + options: FlutterFormOptions( + pages: [ + FlutterFormPage( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translations.paymenttitle, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 4, + ), + Text( + translations.paymentExplainer, + style: theme.textTheme.bodyMedium, + ), + const SizedBox( + height: 84, + ), + FlutterFormInputMultipleChoice( + crossAxisCount: 1, + mainAxisSpacing: 24, + crossAxisSpacing: 5, + childAspectRatio: 2, + height: 422, + controller: FlutterFormInputMultipleChoiceController( + id: "payment", + mandatory: true, + ), + options: [translations.payNow, translations.payLater], + builder: ( + context, + index, + selected, + controller, + options, + state, + ) => + GestureDetector( + onTap: () { + state.didChange(options[index]); + selected.value = index; + controller.onSaved(options[index]); + }, + child: Container( + decoration: BoxDecoration( + color: selected.value == index + ? Theme.of(context).colorScheme.primary + : Colors.white, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Theme.of(context).colorScheme.primary, + ), + ), + height: 40, + child: Center(child: Text(options[index])), + ), + ), + validationMessage: translations.paymentError, + ), + ], + ), + ), + ), + ], + nextButton: (pageNumber, checkingPages) => Padding( + padding: const EdgeInsets.only(bottom: 32), + child: Align( + alignment: Alignment.bottomCenter, + child: PrimaryButton( + text: translations.paymentButton, + onPressed: () async { + await paymentOptionsController.autoNextStep(); + }, + ), + ), + ), + onFinished: (values) { + onFinished(values.entries.first.value); + }, + onNext: (page, values) {}, + ), + ), + ); + } +} diff --git a/packages/flutter_shopping/lib/src/screens/personal_information_screen.dart b/packages/flutter_shopping/lib/src/screens/personal_information_screen.dart new file mode 100644 index 0000000..7990f21 --- /dev/null +++ b/packages/flutter_shopping/lib/src/screens/personal_information_screen.dart @@ -0,0 +1,256 @@ +import "package:flutter/material.dart"; +import "package:flutter_form_wizard/flutter_form.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; + +/// The personal information page. +class PersonalInformationScreen extends StatelessWidget { + /// Constructor for the PersonalInformationScreen. + const PersonalInformationScreen({ + required this.translations, + required this.onFinished, + required this.options, + super.key, + }); + + /// The translations. + final ShoppingTranslations translations; + + /// The on finished function. + final Function(Map) onFinished; + + /// The options. + final FlutterShoppingOptions options; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var personalInforamtionController = FlutterFormController(); + + InputDecoration inputDecoration(String hint) => InputDecoration( + hintStyle: theme.textTheme.bodySmall, + hintText: hint, + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + ); + return Scaffold( + appBar: AppBar( + title: Text(translations.personalInformationTitle), + ), + body: FlutterForm( + formController: personalInforamtionController, + options: FlutterFormOptions( + pages: [ + FlutterFormPage( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translations.personalInformationName, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 4, + ), + FlutterFormInputPlainText( + decoration: options.informationInputDecoration + ?.call(context, translations.nameHint) ?? + inputDecoration(translations.nameHint), + style: theme.textTheme.bodySmall, + controller: FlutterFormInputPlainTextController( + id: "name", + mandatory: true, + ), + validationMessage: translations.nameRequired, + ), + const SizedBox( + height: 16, + ), + Text( + translations.personalInformationAddress, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 4, + ), + FlutterFormInputPlainText( + decoration: options.informationInputDecoration + ?.call(context, translations.addressHint) ?? + inputDecoration(translations.addressHint), + style: theme.textTheme.bodySmall, + controller: FlutterFormInputPlainTextController( + id: "street", + mandatory: true, + ), + validationMessage: translations.addressRequired, + validator: (value) { + if (value == null || value.isEmpty) { + return translations.addressRequired; + } + var regex = RegExp(r"^[A-Za-z]+\s[0-9]{1,3}$"); + if (!regex.hasMatch(value)) { + return translations.invalidAdress; + } + return null; + }, + ), + const SizedBox( + height: 4, + ), + FlutterFormInputPlainText( + decoration: options.informationInputDecoration + ?.call(context, translations.postalCodeHint) ?? + inputDecoration(translations.postalCodeHint), + style: theme.textTheme.bodySmall, + controller: FlutterFormInputPlainTextController( + id: "postalCode", + mandatory: true, + ), + validationMessage: translations.postalCodeRequired, + validator: (value) { + if (value == null || value.isEmpty) { + return translations.postalCodeRequired; + } + var regex = RegExp(r"^[0-9]{4}[A-Za-z]{2}$"); + if (!regex.hasMatch(value)) { + return translations.invalidPostalCode; + } + return null; + }, + ), + const SizedBox( + height: 4, + ), + FlutterFormInputPlainText( + decoration: options.informationInputDecoration + ?.call(context, translations.cityHint) ?? + inputDecoration(translations.cityHint), + style: theme.textTheme.bodySmall, + controller: FlutterFormInputPlainTextController( + id: "city", + mandatory: true, + ), + validationMessage: translations.cityRequired, + ), + const SizedBox( + height: 16, + ), + Text( + translations.personalInformationPhone, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 4, + ), + FlutterFormInputPhone( + numberFieldStyle: theme.textTheme.bodySmall, + textAlignVertical: TextAlignVertical.center, + decoration: options.informationInputDecoration + ?.call(context, translations.phoneHint) ?? + inputDecoration(translations.phoneHint), + controller: FlutterFormInputPhoneController( + id: "phone", + mandatory: true, + ), + validationMessage: translations.phoneRequired, + validator: (value) { + if (value == null || value.number!.isEmpty) { + return translations.phoneRequired; + } + + // Remove any spaces or hyphens from the input + var phoneNumber = + value.number!.replaceAll(RegExp(r"\s+|-"), ""); + + // Check the length of the remaining digits + if (phoneNumber.length != 10 && + phoneNumber.length != 11) { + return translations.invalidPhoneLength; + } + + // Check if all remaining characters are digits + if (!phoneNumber + .substring(1) + .contains(RegExp(r"^[0-9]*$"))) { + return translations.phoneContainsLettersError; + } + + // If all checks pass, return null (no error) + return null; + }, + ), + const SizedBox( + height: 16, + ), + Text( + translations.personalInformationEmail, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 4, + ), + FlutterFormInputEmail( + style: theme.textTheme.bodySmall, + decoration: options.informationInputDecoration + ?.call(context, translations.emailHint) ?? + inputDecoration(translations.emailHint), + controller: FlutterFormInputEmailController( + id: "email", + mandatory: true, + ), + validationMessage: translations.emailRequired, + ), + const SizedBox( + height: 16, + ), + Text( + translations.personalInformationComment, + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 4, + ), + FlutterFormInputPlainText( + decoration: options.informationInputDecoration + ?.call(context, translations.commentHint) ?? + inputDecoration(translations.commentHint), + style: theme.textTheme.bodySmall, + controller: FlutterFormInputPlainTextController( + id: "comments", + ), + validationMessage: "", + ), + const SizedBox( + height: 100, + ), + ], + ), + ), + ), + ], + nextButton: (pageNumber, checkingPages) => Padding( + padding: const EdgeInsets.only(bottom: 32), + child: Align( + alignment: Alignment.bottomCenter, + child: PrimaryButton( + text: translations.personalInformationButton, + onPressed: () async { + await personalInforamtionController.autoNextStep(); + }, + ), + ), + ), + onFinished: (values) { + onFinished(values.entries.first.value); + }, + onNext: (page, values) {}, + ), + ), + ); + } +} diff --git a/packages/flutter_shopping/lib/src/screens/shopping_cart_screen.dart b/packages/flutter_shopping/lib/src/screens/shopping_cart_screen.dart new file mode 100644 index 0000000..9b59ba3 --- /dev/null +++ b/packages/flutter_shopping/lib/src/screens/shopping_cart_screen.dart @@ -0,0 +1,146 @@ +import "package:flutter/material.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; + +/// The shopping cart screen. +class ShoppingCartScreen extends StatelessWidget { + /// Constructor for the shopping cart screen. + const ShoppingCartScreen({ + required this.shoppingService, + required this.translations, + required this.onOrder, + required this.options, + super.key, + }); + + /// The shopping service. + final ShoppingService shoppingService; + + /// The translations. + final ShoppingTranslations translations; + + /// the on order callback. + final Function() onOrder; + + /// The options. + final FlutterShoppingOptions options; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + title: Text(translations.shoppingCartTitle), + ), + body: StreamBuilder( + stream: shoppingService.getShoppingCart(), + builder: (context, snapshot) { + if (snapshot.hasData) { + var cart = snapshot.data!; + return Stack( + children: [ + SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translations.shoppingCartProducts, + style: theme.textTheme.titleLarge, + ), + Column( + children: cart.products + .map( + (product) => + options.shoppingCartItemBuilder?.call( + context, + product, + () async { + await shoppingService + .addProductToCart(product); + }, + () async { + await shoppingService + .removeProductFromCart(product); + }, + ) ?? + ShoppingCartItem( + options: options, + translations: translations, + product: product, + onAddToCart: (product) async { + await shoppingService + .addProductToCart(product); + }, + onRemoveFromCart: (product) async { + await shoppingService + .removeProductFromCart(product); + }, + ), + ) + .toList(), + ), + const SizedBox( + height: 200, + ), + ], + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: ColoredBox( + color: theme.scaffoldBackgroundColor, + child: Padding( + padding: const EdgeInsets.only( + bottom: 32, + left: 64, + right: 64, + top: 16, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + translations.shoppingCartTotal, + style: theme.textTheme.titleMedium, + ), + Text( + "${translations.shoppingCartCurrency}" + // ignore: lines_longer_than_80_chars + " ${cart.totalAmountWithDiscount.toStringAsFixed(2)}", + style: theme.textTheme.titleMedium, + ), + ], + ), + const SizedBox( + height: 24, + ), + options.primaryButtonBuilder?.call( + context, + cart.products.isNotEmpty, + translations.orderButton, + onOrder, + ) ?? + PrimaryButton( + enabled: cart.products.isNotEmpty, + text: translations.orderButton, + onPressed: onOrder, + ), + ], + ), + ), + ), + ), + ], + ); + } + return const SizedBox.shrink(); + }, + ), + ); + } +} diff --git a/packages/flutter_shopping/lib/src/screens/shopping_screen.dart b/packages/flutter_shopping/lib/src/screens/shopping_screen.dart new file mode 100644 index 0000000..0d52233 --- /dev/null +++ b/packages/flutter_shopping/lib/src/screens/shopping_screen.dart @@ -0,0 +1,252 @@ +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; + +/// The shopping screen. +class ShoppingScreen extends StatefulWidget { + /// Constructor for the shopping screen. + const ShoppingScreen({ + required this.shoppingService, + required this.translations, + required this.onShoppingCartPressed, + required this.options, + this.onFilterPressed, + this.initialShopId, + super.key, + }); + + /// The shopping service. + final ShoppingService shoppingService; + + /// The translations. + final ShoppingTranslations translations; + + /// The initial shop id. + final String? initialShopId; + + /// The on filter pressed function. + final Function()? onFilterPressed; + + /// The on shopping cart pressed function. + final Function() onShoppingCartPressed; + + /// The options for shopping. + final FlutterShoppingOptions options; + + @override + State createState() => _ShoppingScreenState(); +} + +class _ShoppingScreenState extends State { + Shop? selectedShop; + List selectedCategories = []; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var service = widget.shoppingService; + var options = widget.options; + return Scaffold( + drawer: options.shoppingScreenDrawer, + appBar: options.shoppingScreenAppbarBuilder?.call( + context, + selectedShop?.adress ?? "", + widget.onFilterPressed, + ) ?? + AppBar( + title: Text( + selectedShop?.adress ?? "", + style: theme.textTheme.headlineLarge, + ), + leading: IconButton( + onPressed: () {}, + icon: const Icon( + Icons.person_rounded, + size: 28, + ), + ), + actions: [ + IconButton( + onPressed: widget.onFilterPressed, + icon: const Icon( + Icons.filter_alt_rounded, + size: 28, + ), + ), + ], + ), + body: Stack( + children: [ + StreamBuilder( + stream: widget.shoppingService.getShops(), + builder: (context, snapshot) { + if (snapshot.hasData) { + selectedShop ??= widget.shoppingService.selectShop( + widget.initialShopId ?? snapshot.data!.first.id, + ); + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: ShopSelector( + shops: snapshot.data!, + options: options, + shoppingService: widget.shoppingService, + onSelected: (shop) { + service.selectShop(shop.id); + setState(() { + selectedShop = shop; + }); + }, + ), + ), + StreamBuilder( + stream: service.getSelectedCategoryStream(), + builder: (context, snapshot) { + if (snapshot.hasData) { + selectedCategories = snapshot.data!; + return Padding( + padding: + const EdgeInsets.symmetric(horizontal: 20), + child: Wrap( + spacing: 8, + children: snapshot.data! + .map( + (category) => + options.filterChipbuilder?.call( + context, category.name, () { + service + .deselectCategory(category.id); + }) ?? + FilterChip( + backgroundColor: theme.primaryColor, + onSelected: (value) {}, + label: Text( + category.name, + style: theme.textTheme.bodyMedium + ?.copyWith( + color: Colors.white, + ), + ), + deleteIcon: const Icon( + Icons.close_rounded, + color: Colors.white, + ), + onDeleted: () { + service.deselectCategory( + category.id, + ); + }, + ), + ) + .toList(), + ), + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + StreamBuilder( + stream: widget.shoppingService + .getProducts(selectedShop!.id), + builder: (context, snapshot) { + if (snapshot.hasData) { + var products = groupBy( + snapshot.data!, + (Product p) => p.category, + ); + var weeklyOffer = service.getWeeklyOffer(); + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 20, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (weeklyOffer != null) ...[ + options.weeklyOfferBuilder?.call( + context, + weeklyOffer, + ) ?? + WeeklyOffer( + product: weeklyOffer, + translations: widget.translations, + ), + ], + Row( + children: [ + Text( + widget.translations + .whatWouldyouLikeToOrder, + style: theme.textTheme.titleLarge, + textAlign: TextAlign.start, + ), + ], + ), + Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 100, + ), + child: ProductItemList( + options: widget.options, + translations: widget.translations, + products: products, + onAddToCart: (product) async { + await service.addProductToCart(product); + }, + ), + ), + ], + ), + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ), + ], + ), + ); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + ), + StreamBuilder( + stream: service.getCartLength(), + builder: (context, snapshot) { + var cartLength = snapshot.data ?? 0; + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.only(bottom: 32), + child: options.primaryButtonBuilder?.call( + context, + cartLength > 0, + widget.translations.viewShoppingCart, () { + widget.onShoppingCartPressed(); + }) ?? + PrimaryButton( + enabled: cartLength > 0, + text: widget.translations.viewShoppingCart, + onPressed: widget.onShoppingCartPressed, + ), + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/packages/flutter_shopping/lib/src/widgets/filter_item.dart b/packages/flutter_shopping/lib/src/widgets/filter_item.dart new file mode 100644 index 0000000..aafc937 --- /dev/null +++ b/packages/flutter_shopping/lib/src/widgets/filter_item.dart @@ -0,0 +1,44 @@ +// ignore_for_file: avoid_positional_boolean_parameters + +import "package:flutter/material.dart"; +import "package:shopping_repository_interface/shopping_repository_interface.dart"; + +/// A filter item. +class FilterItem extends StatelessWidget { + /// Constructor for the filter item. + const FilterItem({ + required this.value, + required this.onChanged, + required this.category, + super.key, + }); + + /// The category. + final Category category; + + /// The value. + final bool value; + + /// The on changed function. + final Function(bool value) onChanged; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: CheckboxListTile( + controlAffinity: ListTileControlAffinity.leading, + value: value, + onChanged: (value) { + onChanged(value ?? false); + }, + shape: const UnderlineInputBorder(), + title: Text( + category.name, + style: theme.textTheme.bodyMedium, + ), + ), + ); + } +} diff --git a/packages/flutter_shopping/lib/src/widgets/info_bottomsheet.dart b/packages/flutter_shopping/lib/src/widgets/info_bottomsheet.dart new file mode 100644 index 0000000..cfab3d3 --- /dev/null +++ b/packages/flutter_shopping/lib/src/widgets/info_bottomsheet.dart @@ -0,0 +1,45 @@ +import "package:flutter/material.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; + +/// A bottomsheet that shows information. +class InfoBottomsheet extends StatelessWidget { + /// Constructor for the InfoBottomsheet. + const InfoBottomsheet({ + required this.productInfo, + required this.translations, + super.key, + }); + + /// The product info. + final String productInfo; + + /// The translations. + final ShoppingTranslations translations; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), + child: Text( + productInfo, + style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: PrimaryButton( + text: translations.closeInfo, + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ); + } +} diff --git a/packages/flutter_shopping/lib/src/widgets/primary_button.dart b/packages/flutter_shopping/lib/src/widgets/primary_button.dart new file mode 100644 index 0000000..22d4624 --- /dev/null +++ b/packages/flutter_shopping/lib/src/widgets/primary_button.dart @@ -0,0 +1,41 @@ +import "package:flutter/material.dart"; + +/// A primary button. +class PrimaryButton extends StatelessWidget { + /// Constructor for the primary button. + const PrimaryButton({ + required this.text, + required this.onPressed, + this.enabled = true, + super.key, + }); + + /// The text. + final String text; + + /// The on pressed function. + final Function() onPressed; + + /// whether the button is enabled. + final bool enabled; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 254, + ), + child: FilledButton( + onPressed: enabled ? onPressed : null, + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + text, + style: theme.textTheme.displayLarge, + ), + ), + ), + ); + } +} diff --git a/packages/flutter_shopping/lib/src/widgets/product_item.dart b/packages/flutter_shopping/lib/src/widgets/product_item.dart new file mode 100644 index 0000000..f90dcf8 --- /dev/null +++ b/packages/flutter_shopping/lib/src/widgets/product_item.dart @@ -0,0 +1,139 @@ +import "package:cached_network_image/cached_network_image.dart"; +import "package:flutter/material.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; + +/// Shop item. +class ProductItem extends StatelessWidget { + /// Constructor for the shop item. + const ProductItem({ + required this.product, + required this.onAddToCart, + required this.translations, + required this.options, + super.key, + }); + + /// The product. + final Product product; + + /// The on add to cart function. + final Function(Product product) onAddToCart; + + /// The translations. + final ShoppingTranslations translations; + + /// The options. + final FlutterShoppingOptions options; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.only(top: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: CachedNetworkImageProvider( + product.imageUrl, + ), + fit: BoxFit.cover, + ), + ), + ), + const SizedBox( + width: 16, + ), + Text( + product.name, + style: theme.textTheme.titleMedium, + ), + IconButton( + onPressed: () { + options.onInfoPressed?.call(product) ?? + showBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(8), + ), + ), + builder: (context) => InfoBottomsheet( + productInfo: product.description, + translations: translations, + ), + ); + }, + icon: Icon( + Icons.info_outline_rounded, + color: theme.primaryColor, + ), + ), + ], + ), + Row( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (product.isDiscounted) ...[ + Text( + product.price.toStringAsFixed(2), + style: theme.textTheme.labelSmall?.copyWith( + decoration: TextDecoration.lineThrough, + ), + ), + Container( + height: 30, + ), + ], + ], + ), + const SizedBox( + width: 8, + ), + Column( + children: [ + Text( + product.isDiscounted + ? product.discountPrice.toStringAsFixed(2) + : product.price.toStringAsFixed(2), + style: theme.textTheme.bodySmall?.copyWith(fontSize: 14), + ), + InkWell( + onTap: () async { + await onAddToCart(product); + }, + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: theme.primaryColor, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + "+", + style: theme.textTheme.titleMedium?.copyWith( + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ], + ), + ); + } +} diff --git a/packages/flutter_shopping/lib/src/widgets/product_item_list.dart b/packages/flutter_shopping/lib/src/widgets/product_item_list.dart new file mode 100644 index 0000000..bb72132 --- /dev/null +++ b/packages/flutter_shopping/lib/src/widgets/product_item_list.dart @@ -0,0 +1,72 @@ +import "package:flutter/material.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; + +/// ProductItemList +class ProductItemList extends StatelessWidget { + /// Constructor for the product item list. + const ProductItemList({ + required this.products, + required this.onAddToCart, + required this.translations, + required this.options, + super.key, + }); + + /// The items. + final Map> products; + + /// The on add to cart function. + final Function(Product) onAddToCart; + + /// The translations. + final ShoppingTranslations translations; + + /// The options. + final FlutterShoppingOptions options; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Column( + children: products.entries + .map( + (entry) => Column( + children: [ + Row( + children: [ + Text( + entry.key, + style: theme.textTheme.titleMedium, + textAlign: TextAlign.start, + ), + ], + ), + Column( + children: entry.value + .map( + (product) => + options.productItemBuilder?.call( + context, + product, + ) ?? + ProductItem( + options: options, + product: product, + translations: translations, + onAddToCart: (product) async { + await onAddToCart(product); + }, + ), + ) + .toList(), + ), + const SizedBox( + height: 20, + ), + ], + ), + ) + .toList(), + ); + } +} diff --git a/packages/flutter_shopping/lib/src/widgets/shop_selector.dart b/packages/flutter_shopping/lib/src/widgets/shop_selector.dart new file mode 100644 index 0000000..69d9464 --- /dev/null +++ b/packages/flutter_shopping/lib/src/widgets/shop_selector.dart @@ -0,0 +1,53 @@ +import "package:flutter/material.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; + +/// Shop selector. +class ShopSelector extends StatelessWidget { + /// Constructor for the shop selector. + const ShopSelector({ + required this.shoppingService, + required this.onSelected, + required this.options, + required this.shops, + super.key, + }); + + /// The shopping service. + final ShoppingService shoppingService; + + /// The on selected function. + final Function(Shop) onSelected; + + /// The options. + final FlutterShoppingOptions options; + + /// + final List shops; + + @override + Widget build(BuildContext context) => SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + Row( + children: shops + .map( + (shop) => Row( + children: [ + const SizedBox(width: 4), + options.shopSelectorItemBuilder + ?.call(context, shop, onSelected) ?? + ShopSelectorItem( + shoppingService: shoppingService, + onSelected: onSelected, + shop: shop, + ), + ], + ), + ) + .toList(), + ), + ], + ), + ); +} diff --git a/packages/flutter_shopping/lib/src/widgets/shop_selector_item.dart b/packages/flutter_shopping/lib/src/widgets/shop_selector_item.dart new file mode 100644 index 0000000..7ba893a --- /dev/null +++ b/packages/flutter_shopping/lib/src/widgets/shop_selector_item.dart @@ -0,0 +1,48 @@ +import "package:flutter/material.dart"; +import "package:shopping_repository_interface/shopping_repository_interface.dart"; + +/// Shop selector item. +class ShopSelectorItem extends StatelessWidget { + /// Constructor for the shop selector item. + const ShopSelectorItem({ + required this.shop, + required this.onSelected, + required this.shoppingService, + super.key, + }); + + /// The shop. + final Shop shop; + + /// The on selected function. + final Function(Shop) onSelected; + + /// The shopping service. + final ShoppingService shoppingService; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + var isSelected = shop.id == shoppingService.getSelectedShop()?.id; + + return InkWell( + onTap: () => onSelected(shop), + child: DecoratedBox( + decoration: BoxDecoration( + border: !isSelected ? Border.all(color: theme.primaryColor) : null, + color: isSelected ? theme.primaryColor : Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(8), + child: Text( + shop.name, + style: isSelected + ? theme.textTheme.titleMedium?.copyWith(color: Colors.white) + : theme.textTheme.bodyMedium, + ), + ), + ), + ); + } +} diff --git a/packages/flutter_shopping/lib/src/widgets/shopping_cart_item.dart b/packages/flutter_shopping/lib/src/widgets/shopping_cart_item.dart new file mode 100644 index 0000000..abddbbe --- /dev/null +++ b/packages/flutter_shopping/lib/src/widgets/shopping_cart_item.dart @@ -0,0 +1,149 @@ +import "package:cached_network_image/cached_network_image.dart"; +import "package:flutter/material.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; + +/// The shopping cart item. +class ShoppingCartItem extends StatelessWidget { + /// Constructor for the shopping cart item. + const ShoppingCartItem({ + required this.product, + required this.onAddToCart, + required this.onRemoveFromCart, + required this.translations, + required this.options, + super.key, + }); + + /// The product. + final Product product; + + /// The on add to cart function. + final Function(Product product) onAddToCart; + + /// The on remove from cart function. + final Function(Product product) onRemoveFromCart; + + /// The translations. + final ShoppingTranslations translations; + + /// The options. + final FlutterShoppingOptions options; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.only(top: 20), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + image: DecorationImage( + image: CachedNetworkImageProvider( + product.imageUrl, + ), + fit: BoxFit.cover, + ), + ), + ), + const SizedBox( + width: 16, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + product.name, + style: theme.textTheme.titleMedium, + ), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () { + options.onInfoPressed?.call(product) ?? + showBottomSheet( + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(8), + ), + ), + builder: (context) => InfoBottomsheet( + productInfo: product.description, + translations: translations, + ), + ); + }, + icon: Icon( + Icons.info_outline_rounded, + color: theme.primaryColor, + ), + ), + ], + ), + ], + ), + Column( + children: [ + Text( + product.isDiscounted + ? product.discountPrice.toStringAsFixed(2) + : product.price.toStringAsFixed(2), + style: theme.textTheme.bodySmall?.copyWith(fontSize: 14), + ), + Row( + children: [ + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () async { + await onRemoveFromCart(product); + }, + icon: const Icon(Icons.remove_rounded), + ), + InkWell( + onTap: () async { + await onAddToCart(product); + }, + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: theme.primaryColor, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + child: Text( + product.selectedAmount.toString(), + style: theme.textTheme.titleMedium?.copyWith( + color: Colors.white, + ), + ), + ), + ), + ), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () async { + await onAddToCart(product); + }, + icon: const Icon(Icons.add_rounded), + ), + ], + ), + ], + ), + ], + ), + ); + } +} diff --git a/packages/flutter_shopping/lib/src/widgets/weekly_offer.dart b/packages/flutter_shopping/lib/src/widgets/weekly_offer.dart new file mode 100644 index 0000000..edfeb1d --- /dev/null +++ b/packages/flutter_shopping/lib/src/widgets/weekly_offer.dart @@ -0,0 +1,85 @@ +import "package:cached_network_image/cached_network_image.dart"; +import "package:flutter/material.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; + +/// Weekly offer. +class WeeklyOffer extends StatelessWidget { + /// Constructor for the weekly offer. + const WeeklyOffer({ + required this.product, + required this.translations, + super.key, + }); + + /// The product. + final Product? product; + + /// The translations. + final ShoppingTranslations translations; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return Padding( + padding: const EdgeInsets.only(bottom: 24), + child: Container( + width: double.infinity, + height: 350, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.black, + width: 1, + ), + image: DecorationImage( + image: CachedNetworkImageProvider( + product!.imageUrl, + ), + fit: BoxFit.fitWidth, + ), + ), + child: Column( + children: [ + Container( + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(8), + topRight: Radius.circular(8), + ), + ), + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + translations.weeklyOffer, + style: theme.textTheme.headlineSmall, + ), + ), + ), + const Spacer(), + Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(8), + bottomRight: Radius.circular(8), + ), + ), + width: double.infinity, + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + "${product!.name}, now for ${product!.discountPrice} each", + style: theme.textTheme.bodyMedium, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/flutter_shopping/pubspec.yaml b/packages/flutter_shopping/pubspec.yaml index 309d6c7..c60bc1e 100644 --- a/packages/flutter_shopping/pubspec.yaml +++ b/packages/flutter_shopping/pubspec.yaml @@ -1,47 +1,37 @@ name: flutter_shopping -description: "A new Flutter project." -publish_to: "none" -version: 2.0.0 +description: "A new Flutter package project." +version: 3.0.0 +homepage: https://github.com/Iconica-Development/flutter_shopping/packages/flutter_shopping +publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ environment: - sdk: ">=3.3.4 <4.0.0" + sdk: ^3.5.1 + flutter: ">=1.17.0" dependencies: flutter: sdk: flutter - go_router: any - flutter_product_page: - git: - url: https://github.com/Iconica-Development/flutter_shopping - ref: 2.0.0 - path: packages/flutter_product_page - flutter_shopping_cart: - git: - url: https://github.com/Iconica-Development/flutter_shopping - ref: 2.0.0 - path: packages/flutter_shopping_cart - flutter_order_details: - git: - url: https://github.com/Iconica-Development/flutter_shopping - ref: 2.0.0 - path: packages/flutter_order_details - flutter_shopping_interface: - git: - url: https://github.com/Iconica-Development/flutter_shopping - ref: 2.0.0 - path: packages/flutter_shopping_interface - flutter_shopping_local: - git: - url: https://github.com/Iconica-Development/flutter_shopping - ref: 2.0.0 - path: packages/flutter_shopping_local + cached_network_image: ^3.4.1 + rxdart: ^0.28.0 + collection: ^1.18.0 + + animated_toggle: + hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ + version: ^0.0.3 + flutter_form_wizard: + hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ + version: ^6.6.0 + + shopping_repository_interface: + hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ + version: ^3.0.0 dev_dependencies: - flutter_test: - sdk: flutter flutter_iconica_analysis: git: url: https://github.com/Iconica-Development/flutter_iconica_analysis ref: 7.0.0 flutter: + assets: + - assets/ diff --git a/packages/flutter_shopping_cart/README.md b/packages/flutter_shopping_cart/README.md deleted file mode 100644 index c9ededd..0000000 --- a/packages/flutter_shopping_cart/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# flutter_shopping_cart - -This component contains a shopping cart screen and the functionality for shopping carts that contain products. - -## Features - -* Shopping cart screen -* Shopping cart products - -## Usage - -First, create your own product by extending the `Product` class: - -```dart -class ExampleProduct extends Product { - ExampleProduct({ - requried super.id, - required super.name, - required super.price, - required this.image, - super.quantity, - }); - - final String image; -} -``` - -Next, you can create the `ShoppingCartScreen` widget like this: - -```dart -var myProductService = ProductService([]); - -ShoppingCartScreen( - configuration: ShoppingCartConfig( - productService: myProductService, - // - productItemBuilder: ( - BuildContext context, - Locale locale, - ExampleProduct product, - ) => - ListTile( - title: Text(product.name), - subtitle: Text(product.price.toString()), - ), - // - onConfirmOrder: (List products) { - print("Placing order with products: $products"); - }, - ), -); -``` - -For a more detailed example you can see the [example](https://github.com/Iconica-Development/flutter_shopping_cart/tree/main/example). - -Or, you could run the example yourself: -``` -git clone https://github.com/Iconica-Development/flutter_shopping_cart.git - -cd flutter_shopping_cart - -cd example - -flutter run -``` - -## Issues - -Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_shopping_cart) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl). - -## Want to contribute - -If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_shopping_cart/pulls). - -## Author - -This flutter_shopping_cart for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at diff --git a/packages/flutter_shopping_cart/analysis_options.yaml b/packages/flutter_shopping_cart/analysis_options.yaml deleted file mode 100644 index 0736605..0000000 --- a/packages/flutter_shopping_cart/analysis_options.yaml +++ /dev/null @@ -1,9 +0,0 @@ -include: package:flutter_iconica_analysis/components_options.yaml - -# Possible to overwrite the rules from the package - -analyzer: - exclude: - -linter: - rules: diff --git a/packages/flutter_shopping_cart/lib/flutter_shopping_cart.dart b/packages/flutter_shopping_cart/lib/flutter_shopping_cart.dart deleted file mode 100644 index 4599843..0000000 --- a/packages/flutter_shopping_cart/lib/flutter_shopping_cart.dart +++ /dev/null @@ -1,6 +0,0 @@ -/// Flutter component for shopping cart. -library flutter_shopping_cart; - -export "src/config/shopping_cart_config.dart"; -export "src/config/shopping_cart_translations.dart"; -export "src/widgets/shopping_cart_screen.dart"; diff --git a/packages/flutter_shopping_cart/lib/src/config/shopping_cart_config.dart b/packages/flutter_shopping_cart/lib/src/config/shopping_cart_config.dart deleted file mode 100644 index 6a5ff3e..0000000 --- a/packages/flutter_shopping_cart/lib/src/config/shopping_cart_config.dart +++ /dev/null @@ -1,89 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// Shopping cart configuration -/// -/// This class is used to configure the shopping cart. -class ShoppingCartConfig { - /// Creates a shopping cart configuration. - ShoppingCartConfig({ - required this.service, - required this.onConfirmOrder, - this.productItemBuilder, - this.confirmOrderButtonBuilder, - this.confirmOrderButtonHeight = 100, - this.sumBottomSheetBuilder, - this.sumBottomSheetHeight = 100, - this.titleBuilder, - this.translations = const ShoppingCartTranslations(), - this.pagePadding = const EdgeInsets.symmetric(horizontal: 32), - this.bottomPadding = const EdgeInsets.fromLTRB(44, 0, 44, 32), - this.appBarBuilder, - }); - - /// Product service. The product service is used to manage the products in the - /// shopping cart. - final ShoppingCartService service; - - /// Product item builder. This builder is used to build the product item - /// that will be displayed in the shopping cart. - Widget Function( - BuildContext context, - Product product, - ShoppingCartConfig configuration, - )? productItemBuilder; - - /// Confirm order button builder. This builder is used to build the confirm - /// order button that will be displayed in the shopping cart. - /// If you override this builder, you cannot use the [onConfirmOrder] callback - Widget Function( - BuildContext context, - ShoppingCartConfig configuration, - Function(List products) onConfirmOrder, - )? confirmOrderButtonBuilder; - - /// Confirm order button height. The height of the confirm order button. - /// This height is used to calculate the bottom padding of the shopping cart. - /// If you override the confirm order button builder, you must provide a - /// height. - double confirmOrderButtonHeight; - - /// Confirm order callback. This callback is called when the confirm order - /// button is pressed. The callback will not be called if you override the - /// confirm order button builder. - final Function(List products) onConfirmOrder; - - /// Sum bottom sheet builder. This builder is used to build the sum bottom - /// sheet that will be displayed in the shopping cart. The sum bottom sheet - /// can be used to display the total sum of the products in the shopping cart. - Widget Function(BuildContext context, ShoppingCartConfig configuration)? - sumBottomSheetBuilder; - - /// Sum bottom sheet height. The height of the sum bottom sheet. - /// This height is used to calculate the bottom padding of the shopping cart. - /// If you override the sum bottom sheet builder, you must provide a height. - double sumBottomSheetHeight; - - /// Padding around the shopping cart. The padding is used to create space - /// around the shopping cart. - EdgeInsets pagePadding; - - /// Bottom padding of the shopping cart. The bottom padding is used to create - /// a padding around the bottom sheet. This padding is ignored when the - /// [sumBottomSheetBuilder] is overridden. - EdgeInsets bottomPadding; - - /// Title builder. This builder is used to - /// build the title of the shopping cart. - final Widget Function( - BuildContext context, - String title, - )? titleBuilder; - - /// Shopping cart translations. The translations for the shopping cart. - ShoppingCartTranslations translations; - - /// Appbar for the shopping cart screen. - PreferredSizeWidget Function(BuildContext context)? appBarBuilder; -} diff --git a/packages/flutter_shopping_cart/lib/src/config/shopping_cart_translations.dart b/packages/flutter_shopping_cart/lib/src/config/shopping_cart_translations.dart deleted file mode 100644 index a6deeb9..0000000 --- a/packages/flutter_shopping_cart/lib/src/config/shopping_cart_translations.dart +++ /dev/null @@ -1,23 +0,0 @@ -/// Shopping cart localizations -class ShoppingCartTranslations { - /// Creates shopping cart localizations - const ShoppingCartTranslations({ - this.placeOrder = "Order", - this.sum = "Subtotal:", - this.cartTitle = "Products", - this.close = "close", - }); - - /// Text for the place order button. - final String placeOrder; - - /// Localization for the sum. - final String sum; - - /// Title for the shopping cart. This title will be displayed at the top of - /// the shopping cart. - final String cartTitle; - - /// Localization for the close button for the popup. - final String close; -} diff --git a/packages/flutter_shopping_cart/lib/src/widgets/default_appbar.dart b/packages/flutter_shopping_cart/lib/src/widgets/default_appbar.dart deleted file mode 100644 index b348975..0000000 --- a/packages/flutter_shopping_cart/lib/src/widgets/default_appbar.dart +++ /dev/null @@ -1,23 +0,0 @@ -import "package:flutter/material.dart"; - -/// Default appbar for the shopping cart. -class DefaultAppbar extends StatelessWidget implements PreferredSizeWidget { - /// Constructor for the default appbar for the shopping cart. - const DefaultAppbar({ - super.key, - }); - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return AppBar( - title: Text( - "Shopping cart", - style: theme.textTheme.headlineLarge, - ), - ); - } - - @override - Size get preferredSize => const Size.fromHeight(kToolbarHeight); -} diff --git a/packages/flutter_shopping_cart/lib/src/widgets/default_confirm_order_button.dart b/packages/flutter_shopping_cart/lib/src/widgets/default_confirm_order_button.dart deleted file mode 100644 index d2a40fd..0000000 --- a/packages/flutter_shopping_cart/lib/src/widgets/default_confirm_order_button.dart +++ /dev/null @@ -1,51 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// Default confirm order button. -class DefaultConfirmOrderButton extends StatelessWidget { - /// Constructor for the default confirm order button. - const DefaultConfirmOrderButton({ - required this.configuration, - required this.onConfirmOrder, - super.key, - }); - - /// Configuration for the shopping cart. - final ShoppingCartConfig configuration; - - /// Function to call when the order is confirmed. - final Function(List products) onConfirmOrder; - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 60), - child: SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: configuration.service.products.isEmpty - ? null - : () => onConfirmOrder( - configuration.service.products, - ), - style: theme.filledButtonTheme.style?.copyWith( - backgroundColor: WidgetStateProperty.all( - theme.colorScheme.primary, - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 12, - ), - child: Text( - configuration.translations.placeOrder, - style: theme.textTheme.displayLarge, - ), - ), - ), - ), - ); - } -} diff --git a/packages/flutter_shopping_cart/lib/src/widgets/default_shopping_cart_item.dart b/packages/flutter_shopping_cart/lib/src/widgets/default_shopping_cart_item.dart deleted file mode 100644 index ba9f89c..0000000 --- a/packages/flutter_shopping_cart/lib/src/widgets/default_shopping_cart_item.dart +++ /dev/null @@ -1,134 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; -import "package:flutter_shopping_cart/src/widgets/product_item_popup.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// Default shopping cart item. -class DefaultShoppingCartItem extends StatelessWidget { - /// Constructor for the default shopping cart item. - const DefaultShoppingCartItem({ - required this.product, - required this.configuration, - required this.onItemAddedRemoved, - super.key, - }); - - /// Product to display. - final Product product; - - /// Shopping cart configuration. - final ShoppingCartConfig configuration; - - /// Function that is called when an item is added or removed. - final Function() onItemAddedRemoved; - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.only(bottom: 20), - child: ListTile( - contentPadding: const EdgeInsets.only(top: 3, left: 4, bottom: 3), - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - product.name, - style: theme.textTheme.titleMedium, - ), - IconButton( - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - onPressed: () async { - await showModalBottomSheet( - context: context, - backgroundColor: theme.colorScheme.surface, - builder: (context) => ProductItemPopup( - product: product, - configuration: configuration, - ), - ); - }, - icon: Icon( - Icons.info_outline, - color: theme.colorScheme.primary, - ), - ), - ], - ), - leading: ClipRRect( - borderRadius: BorderRadius.circular(6), - child: Image.network( - product.imageUrl, - ), - ), - trailing: Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (product.hasDiscount && product.discountPrice != null) ...[ - Text( - product.discountPrice!.toStringAsFixed(2), - style: theme.textTheme.labelSmall, - ), - ] else ...[ - Text( - product.price.toStringAsFixed(2), - style: theme.textTheme.labelSmall, - ), - ], - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - constraints: const BoxConstraints(), - padding: EdgeInsets.zero, - icon: const Icon( - Icons.remove, - color: Colors.black, - ), - onPressed: () { - configuration.service.removeOneProduct(product); - onItemAddedRemoved(); - }, - ), - Padding( - padding: const EdgeInsets.all(2), - child: Container( - alignment: Alignment.center, - decoration: BoxDecoration( - color: theme.colorScheme.primary, - borderRadius: BorderRadius.circular(4), - ), - height: 30, - width: 30, - child: Text( - "${product.quantity}", - style: theme.textTheme.titleSmall, - textAlign: TextAlign.center, - ), - ), - ), - IconButton( - constraints: const BoxConstraints(), - padding: EdgeInsets.zero, - icon: const Icon( - Icons.add, - color: Colors.black, - ), - onPressed: () { - configuration.service.addProduct(product); - onItemAddedRemoved(); - }, - ), - ], - ), - ], - ), - ), - ); - } -} diff --git a/packages/flutter_shopping_cart/lib/src/widgets/default_sum_bottom_sheet_builder.dart b/packages/flutter_shopping_cart/lib/src/widgets/default_sum_bottom_sheet_builder.dart deleted file mode 100644 index f597e29..0000000 --- a/packages/flutter_shopping_cart/lib/src/widgets/default_sum_bottom_sheet_builder.dart +++ /dev/null @@ -1,42 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; - -/// Default sum bottom sheet builder. -class DefaultSumBottomSheetBuilder extends StatelessWidget { - /// Constructor for the default sum bottom sheet builder. - const DefaultSumBottomSheetBuilder({ - required this.configuration, - super.key, - }); - - /// Configuration for the shopping cart. - final ShoppingCartConfig configuration; - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - var totalPrice = configuration.service.products.fold( - 0, - (previousValue, element) => - previousValue + - (element.discountPrice ?? element.price) * element.quantity, - ); - - return Padding( - padding: configuration.bottomPadding, - child: Row( - children: [ - Text( - configuration.translations.sum, - style: theme.textTheme.titleMedium, - ), - const Spacer(), - Text( - "€ ${totalPrice.toStringAsFixed(2)}", - style: theme.textTheme.bodyMedium, - ), - ], - ), - ); - } -} diff --git a/packages/flutter_shopping_cart/lib/src/widgets/product_item_popup.dart b/packages/flutter_shopping_cart/lib/src/widgets/product_item_popup.dart deleted file mode 100644 index ebe32c0..0000000 --- a/packages/flutter_shopping_cart/lib/src/widgets/product_item_popup.dart +++ /dev/null @@ -1,66 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// A popup that displays the product item. -class ProductItemPopup extends StatelessWidget { - /// Constructor for the product item popup. - const ProductItemPopup({ - required this.product, - required this.configuration, - super.key, - }); - - /// The product to display. - final Product product; - - /// Configuration for the product page. - final ShoppingCartConfig configuration; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - return SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(32), - child: SizedBox( - width: double.infinity, - child: Column( - children: [ - Text( - product.description, - style: theme.textTheme.bodySmall, - textAlign: TextAlign.center, - ), - Padding( - padding: const EdgeInsets.only(top: 20, left: 40, right: 40), - child: SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: () => Navigator.of(context).pop(), - style: theme.filledButtonTheme.style?.copyWith( - backgroundColor: WidgetStateProperty.all( - theme.colorScheme.primary, - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: Text( - configuration.translations.close, - style: theme.textTheme.displayLarge, - ), - ), - ), - ), - ), - ], - ), - ), - ), - ); - } -} diff --git a/packages/flutter_shopping_cart/lib/src/widgets/shopping_cart_screen.dart b/packages/flutter_shopping_cart/lib/src/widgets/shopping_cart_screen.dart deleted file mode 100644 index 3337de1..0000000 --- a/packages/flutter_shopping_cart/lib/src/widgets/shopping_cart_screen.dart +++ /dev/null @@ -1,131 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; -import "package:flutter_shopping_cart/src/widgets/default_appbar.dart"; -import "package:flutter_shopping_cart/src/widgets/default_confirm_order_button.dart"; -import "package:flutter_shopping_cart/src/widgets/default_shopping_cart_item.dart"; -import "package:flutter_shopping_cart/src/widgets/default_sum_bottom_sheet_builder.dart"; - -/// Shopping cart screen widget. -class ShoppingCartScreen extends StatefulWidget { - /// Creates a shopping cart screen. - const ShoppingCartScreen({ - required this.configuration, - super.key, - }); - - /// Configuration for the shopping cart screen. - final ShoppingCartConfig configuration; - - @override - State createState() => _ShoppingCartScreenState(); -} - -class _ShoppingCartScreenState extends State { - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - return Scaffold( - appBar: widget.configuration.appBarBuilder?.call(context) ?? - const DefaultAppbar(), - body: SafeArea( - child: Stack( - fit: StackFit.expand, - children: [ - Padding( - padding: widget.configuration.pagePadding, - child: SingleChildScrollView( - child: Column( - children: [ - if (widget.configuration.titleBuilder != null) ...{ - widget.configuration.titleBuilder!( - context, - widget.configuration.translations.cartTitle, - ), - } else ...{ - Padding( - padding: const EdgeInsets.symmetric( - vertical: 32, - ), - child: Row( - children: [ - Text( - widget.configuration.translations.cartTitle, - style: theme.textTheme.titleLarge, - textAlign: TextAlign.start, - ), - ], - ), - ), - }, - Column( - children: [ - for (var product - in widget.configuration.service.products) - widget.configuration.productItemBuilder?.call( - context, - product, - widget.configuration, - ) ?? - DefaultShoppingCartItem( - product: product, - configuration: widget.configuration, - onItemAddedRemoved: () { - setState(() {}); - }, - ), - - // Additional whitespace at - // the bottom to make sure the last - // product(s) are not hidden by the bottom sheet. - SizedBox( - height: - widget.configuration.confirmOrderButtonHeight + - widget.configuration.sumBottomSheetHeight, - ), - ], - ), - ], - ), - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: _BottomSheet( - configuration: widget.configuration, - ), - ), - ], - ), - ), - ); - } -} - -class _BottomSheet extends StatelessWidget { - const _BottomSheet({ - required this.configuration, - }); - - final ShoppingCartConfig configuration; - - @override - Widget build(BuildContext context) => Column( - mainAxisSize: MainAxisSize.min, - children: [ - configuration.sumBottomSheetBuilder?.call(context, configuration) ?? - DefaultSumBottomSheetBuilder( - configuration: configuration, - ), - configuration.confirmOrderButtonBuilder?.call( - context, - configuration, - configuration.onConfirmOrder, - ) ?? - DefaultConfirmOrderButton( - configuration: configuration, - onConfirmOrder: configuration.onConfirmOrder, - ), - ], - ); -} diff --git a/packages/flutter_shopping_cart/pubspec.yaml b/packages/flutter_shopping_cart/pubspec.yaml deleted file mode 100644 index d8860e9..0000000 --- a/packages/flutter_shopping_cart/pubspec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: flutter_shopping_cart -description: "A Flutter module for a shopping cart." -version: 2.0.0 -publish_to: "none" - -environment: - sdk: ">=3.3.0 <4.0.0" - flutter: ">=1.17.0" - -dependencies: - flutter: - sdk: flutter - flutter_shopping_interface: - git: - url: https://github.com/Iconica-Development/flutter_shopping - path: packages/flutter_shopping_interface - ref: 2.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 - -flutter: diff --git a/packages/flutter_shopping_interface/analysis_options.yaml b/packages/flutter_shopping_interface/analysis_options.yaml deleted file mode 100644 index 0736605..0000000 --- a/packages/flutter_shopping_interface/analysis_options.yaml +++ /dev/null @@ -1,9 +0,0 @@ -include: package:flutter_iconica_analysis/components_options.yaml - -# Possible to overwrite the rules from the package - -analyzer: - exclude: - -linter: - rules: diff --git a/packages/flutter_shopping_interface/lib/flutter_shopping_interface.dart b/packages/flutter_shopping_interface/lib/flutter_shopping_interface.dart deleted file mode 100644 index 712a716..0000000 --- a/packages/flutter_shopping_interface/lib/flutter_shopping_interface.dart +++ /dev/null @@ -1,2 +0,0 @@ -export "src/model/model.dart"; -export "src/service/service.dart"; diff --git a/packages/flutter_shopping_interface/lib/src/model/model.dart b/packages/flutter_shopping_interface/lib/src/model/model.dart deleted file mode 100644 index a77eeaa..0000000 --- a/packages/flutter_shopping_interface/lib/src/model/model.dart +++ /dev/null @@ -1,2 +0,0 @@ -export "product.dart"; -export "shop.dart"; diff --git a/packages/flutter_shopping_interface/lib/src/model/product.dart b/packages/flutter_shopping_interface/lib/src/model/product.dart deleted file mode 100644 index 2ba1599..0000000 --- a/packages/flutter_shopping_interface/lib/src/model/product.dart +++ /dev/null @@ -1,85 +0,0 @@ -/// Product Interface -abstract class ProductInterface { - /// ProductInterface constructor - ProductInterface({ - required this.id, - required this.name, - required this.imageUrl, - required this.category, - required this.price, - required this.description, - this.hasDiscount = false, - this.discountPrice, - this.quantity = 1, - }); - - /// Product id - final String id; - - /// Product name - final String name; - - /// Product image url - final String imageUrl; - - /// Product category - final String category; - - /// Product price - final double price; - - /// whether the product has a discount - final bool hasDiscount; - - /// Product discount price - final double? discountPrice; - - /// Product quantity - int quantity; - - /// Product description - final String description; -} - -/// Product model -class Product implements ProductInterface { - /// Product constructor - Product({ - required this.id, - required this.name, - required this.imageUrl, - required this.category, - required this.price, - required this.description, - this.hasDiscount = false, - this.discountPrice, - this.quantity = 1, - }); - - @override - final String id; - - @override - final String name; - - @override - final String imageUrl; - - @override - final String category; - - @override - final double price; - - @override - final bool hasDiscount; - - @override - final double? discountPrice; - - @override - int quantity; - - @override - final String description; -} diff --git a/packages/flutter_shopping_interface/lib/src/model/shop.dart b/packages/flutter_shopping_interface/lib/src/model/shop.dart deleted file mode 100644 index b85dd6c..0000000 --- a/packages/flutter_shopping_interface/lib/src/model/shop.dart +++ /dev/null @@ -1,28 +0,0 @@ -/// Shop interface -abstract class ShopInterface { - /// ShopInterface constructor - const ShopInterface({ - required this.id, - required this.name, - }); - - /// Shop id - final String id; - - /// Shop name - final String name; -} - -/// Shop model -class Shop implements ShopInterface { - /// Shop constructor - const Shop({ - required this.id, - required this.name, - }); - @override - final String id; - - @override - final String name; -} diff --git a/packages/flutter_shopping_interface/lib/src/service/order_service.dart b/packages/flutter_shopping_interface/lib/src/service/order_service.dart deleted file mode 100644 index 2904d0d..0000000 --- a/packages/flutter_shopping_interface/lib/src/service/order_service.dart +++ /dev/null @@ -1,12 +0,0 @@ -import "package:flutter_shopping_interface/src/model/product.dart"; - -/// Order service -// ignore: one_member_abstracts -abstract class OrderService { - /// Create an order - Future createOrder( - String shopId, - List products, - Map> clientInformation, - ); -} diff --git a/packages/flutter_shopping_interface/lib/src/service/product_service.dart b/packages/flutter_shopping_interface/lib/src/service/product_service.dart deleted file mode 100644 index 82abee1..0000000 --- a/packages/flutter_shopping_interface/lib/src/service/product_service.dart +++ /dev/null @@ -1,23 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_interface/src/model/product.dart"; - -/// Product service -abstract class ProductService with ChangeNotifier { - /// Retrieve a list of products - Future> getProducts(String shopId); - - /// Retrieve a product - Future getProduct(String id); - - /// Retrieve a list of categories - List getCategories(); - - /// Get current Products - List get products; - - /// Get current Products - List get selectedCategories; - - /// Select a category - void selectCategory(String category); -} diff --git a/packages/flutter_shopping_interface/lib/src/service/service.dart b/packages/flutter_shopping_interface/lib/src/service/service.dart deleted file mode 100644 index 6d20b45..0000000 --- a/packages/flutter_shopping_interface/lib/src/service/service.dart +++ /dev/null @@ -1,5 +0,0 @@ -export "order_service.dart"; -export "product_service.dart"; -export "shop_service.dart"; -export "shopping_cart_service.dart"; -export "shopping_service.dart"; diff --git a/packages/flutter_shopping_interface/lib/src/service/shop_service.dart b/packages/flutter_shopping_interface/lib/src/service/shop_service.dart deleted file mode 100644 index 205f4c4..0000000 --- a/packages/flutter_shopping_interface/lib/src/service/shop_service.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_interface/src/model/shop.dart"; - -/// Shop service -// ignore: one_member_abstracts -abstract class ShopService with ChangeNotifier { - /// Retrieve a list of shops - Future> getShops(); - - /// Select a shop - void selectShop(Shop shop); - - /// The currently selected shop - Shop? get selectedShop; -} diff --git a/packages/flutter_shopping_interface/lib/src/service/shopping_cart_service.dart b/packages/flutter_shopping_interface/lib/src/service/shopping_cart_service.dart deleted file mode 100644 index 6c2ce51..0000000 --- a/packages/flutter_shopping_interface/lib/src/service/shopping_cart_service.dart +++ /dev/null @@ -1,23 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_interface/src/model/product.dart"; - -/// shopping cart service -abstract class ShoppingCartService with ChangeNotifier { - /// Adds a product to the shopping cart. - void addProduct(Product product); - - /// Removes a product from the shopping cart. - void removeProduct(Product product); - - /// Removes one product from the shopping cart. - void removeOneProduct(Product product); - - /// Counts the number of products in the shopping cart. - int countProducts(); - - /// Clears the shopping cart. - void clear(); - - /// The list of products in the shopping cart. - List get products; -} diff --git a/packages/flutter_shopping_interface/lib/src/service/shopping_service.dart b/packages/flutter_shopping_interface/lib/src/service/shopping_service.dart deleted file mode 100644 index e806f62..0000000 --- a/packages/flutter_shopping_interface/lib/src/service/shopping_service.dart +++ /dev/null @@ -1,24 +0,0 @@ -import "package:flutter_shopping_interface/src/service/service.dart"; - -/// Shopping service -class ShoppingService { - /// Shopping service constructor - const ShoppingService({ - required this.orderService, - required this.productService, - required this.shopService, - required this.shoppingCartService, - }); - - /// Order service - final OrderService orderService; - - /// Product service - final ProductService productService; - - /// Shop service - final ShopService shopService; - - /// Shopping cart service - final ShoppingCartService shoppingCartService; -} diff --git a/packages/flutter_shopping_interface/pubspec.yaml b/packages/flutter_shopping_interface/pubspec.yaml deleted file mode 100644 index ad4a08c..0000000 --- a/packages/flutter_shopping_interface/pubspec.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: flutter_shopping_interface -description: "A Flutter module for a shopping." -version: 2.0.0 -publish_to: 'none' - -environment: - sdk: '>=3.3.0 <4.0.0' - flutter: ">=1.17.0" - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 - -flutter: \ No newline at end of file diff --git a/packages/flutter_shopping_local/analysis_options.yaml b/packages/flutter_shopping_local/analysis_options.yaml deleted file mode 100644 index 0736605..0000000 --- a/packages/flutter_shopping_local/analysis_options.yaml +++ /dev/null @@ -1,9 +0,0 @@ -include: package:flutter_iconica_analysis/components_options.yaml - -# Possible to overwrite the rules from the package - -analyzer: - exclude: - -linter: - rules: diff --git a/packages/flutter_shopping_local/lib/flutter_shopping_local.dart b/packages/flutter_shopping_local/lib/flutter_shopping_local.dart deleted file mode 100644 index 44941b5..0000000 --- a/packages/flutter_shopping_local/lib/flutter_shopping_local.dart +++ /dev/null @@ -1 +0,0 @@ -export "service/service.dart"; diff --git a/packages/flutter_shopping_local/lib/service/local_order_service.dart b/packages/flutter_shopping_local/lib/service/local_order_service.dart deleted file mode 100644 index 3f84868..0000000 --- a/packages/flutter_shopping_local/lib/service/local_order_service.dart +++ /dev/null @@ -1,15 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// Local order service -class LocalOrderService with ChangeNotifier implements OrderService { - @override - Future createOrder( - String shopId, - List products, - Map> clientInformation, - ) async { - // Create the order - notifyListeners(); - } -} diff --git a/packages/flutter_shopping_local/lib/service/local_product_service.dart b/packages/flutter_shopping_local/lib/service/local_product_service.dart deleted file mode 100644 index bf67bef..0000000 --- a/packages/flutter_shopping_local/lib/service/local_product_service.dart +++ /dev/null @@ -1,103 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// Local product service -class LocalProductService with ChangeNotifier implements ProductService { - List _products = []; - List _allProducts = []; - final List _selectedCategories = []; - - @override - List getCategories() => - _allProducts.map((e) => e.category).toSet().toList(); - - @override - Future getProduct(String id) => - Future.value(_products.firstWhere((element) => element.id == id)); - - @override - Future> getProducts(String shopId) async { - await Future.delayed(const Duration(seconds: 1)); - _products = [ - Product( - id: "1", - name: "White Bread", - imageUrl: "https://firebasestorage.googleapis.com/v0/b/appshell-demo" - ".appspot.com/o/shopping%2Fwhite.png" - "?alt=media&token=e3aa13d5-932a-4119-bdbb-89c1a1d82213", - category: "Bread", - price: 1.0, - description: "This is a delicious white bread", - hasDiscount: true, - discountPrice: 0.5, - ), - Product( - id: "2", - name: "Brown Bread", - imageUrl: "https://firebasestorage.googleapis.com/v0/b/appshell-" - "demo.appspot.com/o/shopping%2Fbrown.png?alt=media&" - "token=fbfe280d-44e6-4cde-a491-bcb7f598d22c", - category: "Bread", - price: 2.0, - description: "This is a delicious brown bread", - ), - Product( - id: "3", - name: "White fish", - imageUrl: "https://firebasestorage.googleapis.com/v0/b/appshell" - "-demo.appspot.com/o/shopping%2Fwhite-fish.png?alt=media" - "&token=61c44f84-347d-4b42-a10d-c172f167929b", - category: "Fish", - price: 1.5, - description: "This is a delicious white fish", - ), - Product( - id: "4", - name: "Brown fish", - imageUrl: "https://firebasestorage.googleapis.com/v0/b/appshel" - "l-demo.appspot.com/o/shopping%2Fbrown-fish.png?alt=media&" - "token=b743a53c-c4bb-49ac-8894-a08132992902", - category: "Fish", - price: 1.5, - description: "This is a delicious Brown fish", - ), - ]; - - // only return items that match the selectedcategories - _allProducts = List.from(_products); - - _products = _products.where((element) { - if (_selectedCategories.isEmpty) { - return true; - } - return _selectedCategories.contains(element.category); - }).toList(); - - return Future.value(_products); - } - - @override - List get products => _products; - - @override - void selectCategory(String category) { - if (_selectedCategories.contains(category)) { - _selectedCategories.remove(category); - } else { - _selectedCategories.add(category); - } - if (_selectedCategories.isEmpty) { - _products = List.from(_allProducts); - } - _products = _allProducts.where((element) { - if (_selectedCategories.isEmpty) { - return true; - } - return _selectedCategories.contains(element.category); - }).toList(); - notifyListeners(); - } - - @override - List get selectedCategories => _selectedCategories; -} diff --git a/packages/flutter_shopping_local/lib/service/local_shop_service.dart b/packages/flutter_shopping_local/lib/service/local_shop_service.dart deleted file mode 100644 index 6c5a11a..0000000 --- a/packages/flutter_shopping_local/lib/service/local_shop_service.dart +++ /dev/null @@ -1,35 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// A service that provides a list of shops. -class LocalShopService with ChangeNotifier implements ShopService { - Shop? _selectedShop; - - @override - Future> getShops() async { - await Future.delayed(const Duration(seconds: 1)); - var shops = [ - const Shop(id: "1", name: "Bakkerij de Goudkorst"), - const Shop(id: "2", name: "Slagerij PuurVlees"), - const Shop(id: "3", name: "De GroenteHut"), - const Shop(id: "4", name: "Pizzeria Ciao"), - const Shop(id: "5", name: "Cafetaria Roos"), - const Shop(id: "6", name: "Zeebries Visdelicatessen"), - const Shop(id: "7", name: "De Oosterse Draak"), - ]; - return Future.value(shops); - } - - /// Updates the selected shop. - @override - void selectShop(Shop shop) { - if (_selectedShop == shop) return; - - _selectedShop = shop; - notifyListeners(); - } - - /// The currently selected shop. - @override - Shop? get selectedShop => _selectedShop; -} diff --git a/packages/flutter_shopping_local/lib/service/local_shopping_cart_service.dart b/packages/flutter_shopping_local/lib/service/local_shopping_cart_service.dart deleted file mode 100644 index d6c944f..0000000 --- a/packages/flutter_shopping_local/lib/service/local_shopping_cart_service.dart +++ /dev/null @@ -1,60 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; - -/// Local shopping cart service -class LocalShoppingCartService - with ChangeNotifier - implements ShoppingCartService { - final List _products = []; - @override - void addProduct(Product product) { - if (_products.contains(product)) { - var index = _products.indexOf(product); - _products[index].quantity++; - } else { - _products.add(product); - } - notifyListeners(); - } - - @override - void clear() { - _products.clear(); - notifyListeners(); - } - - @override - int countProducts() { - var count = 0; - for (var product in _products) { - count += product.quantity; - } - notifyListeners(); - return count; - } - - @override - void removeOneProduct(Product product) { - if (_products.contains(product)) { - var index = _products.indexOf(product); - if (_products[index].quantity > 1) { - _products[index].quantity--; - } else { - _products.removeAt(index); - } - } - notifyListeners(); - } - - @override - void removeProduct(Product product) { - if (_products.contains(product)) { - var index = _products.indexOf(product); - _products.removeAt(index); - } - notifyListeners(); - } - - @override - List get products => _products; -} diff --git a/packages/flutter_shopping_local/lib/service/local_shopping_service.dart b/packages/flutter_shopping_local/lib/service/local_shopping_service.dart deleted file mode 100644 index ef91410..0000000 --- a/packages/flutter_shopping_local/lib/service/local_shopping_service.dart +++ /dev/null @@ -1,45 +0,0 @@ -import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; -import "package:flutter_shopping_local/service/local_order_service.dart"; -import "package:flutter_shopping_local/service/local_product_service.dart"; -import "package:flutter_shopping_local/service/local_shop_service.dart"; -import "package:flutter_shopping_local/service/local_shopping_cart_service.dart"; - -/// Local shopping service -class LocalShoppingService implements ShoppingService { - /// Local shopping service constructor - LocalShoppingService({ - this.localOrderService, - this.localShopService, - this.localProductService, - this.localShoppingCartService, - }) { - localOrderService ??= LocalOrderService(); - localShopService ??= LocalShopService(); - localProductService ??= LocalProductService(); - localShoppingCartService ??= LocalShoppingCartService(); - } - - /// Local order service - OrderService? localOrderService; - - /// Local shop service - ShopService? localShopService; - - /// Local product service - ProductService? localProductService; - - /// Local shopping cart service - ShoppingCartService? localShoppingCartService; - - @override - OrderService get orderService => localOrderService!; - - @override - ProductService get productService => localProductService!; - - @override - ShopService get shopService => localShopService!; - - @override - ShoppingCartService get shoppingCartService => localShoppingCartService!; -} diff --git a/packages/flutter_shopping_local/lib/service/service.dart b/packages/flutter_shopping_local/lib/service/service.dart deleted file mode 100644 index de7f343..0000000 --- a/packages/flutter_shopping_local/lib/service/service.dart +++ /dev/null @@ -1,5 +0,0 @@ -export "local_order_service.dart"; -export "local_product_service.dart"; -export "local_shop_service.dart"; -export "local_shopping_cart_service.dart"; -export "local_shopping_service.dart"; diff --git a/packages/flutter_shopping_local/pubspec.yaml b/packages/flutter_shopping_local/pubspec.yaml deleted file mode 100644 index a4c1521..0000000 --- a/packages/flutter_shopping_local/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: flutter_shopping_local -description: "A local Flutter module for a shopping." -version: 2.0.0 -publish_to: 'none' - -environment: - sdk: '>=3.3.0 <4.0.0' - flutter: ">=1.17.0" - -dependencies: - flutter: - sdk: flutter - flutter_shopping_interface: - git: - url: https://github.com/Iconica-Development/flutter_shopping - path: packages/flutter_shopping_interface - ref: 2.0.0 - - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_iconica_analysis: - git: - url: https://github.com/Iconica-Development/flutter_iconica_analysis - ref: 7.0.0 - -flutter: \ No newline at end of file diff --git a/packages/flutter_product_page/.gitignore b/packages/shopping_repository_interface/.gitignore similarity index 50% rename from packages/flutter_product_page/.gitignore rename to packages/shopping_repository_interface/.gitignore index a81f4bf..ac5aa98 100644 --- a/packages/flutter_product_page/.gitignore +++ b/packages/shopping_repository_interface/.gitignore @@ -9,7 +9,6 @@ .history .svn/ migrate_working_dir/ -.metadata # IntelliJ related *.iml @@ -20,37 +19,11 @@ migrate_working_dir/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -.vscode/ +#.vscode/ # Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock **/doc/api/ -**/ios/Flutter/.last_build_id .dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ -pubspec.lock - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release - -# env -*dotenv - -android/ -ios/ -linux/ -macos/ -web/ -windows/ \ No newline at end of file +build/ diff --git a/packages/flutter_order_details/analysis_options.yaml b/packages/shopping_repository_interface/analysis_options.yaml similarity index 100% rename from packages/flutter_order_details/analysis_options.yaml rename to packages/shopping_repository_interface/analysis_options.yaml diff --git a/packages/shopping_repository_interface/lib/shopping_repository_interface.dart b/packages/shopping_repository_interface/lib/shopping_repository_interface.dart new file mode 100644 index 0000000..e59a75e --- /dev/null +++ b/packages/shopping_repository_interface/lib/shopping_repository_interface.dart @@ -0,0 +1,24 @@ +// ignore_for_file: directives_ordering + +/// models +library; + +export "src/models/shop.dart"; +export "src/models/product.dart"; +export "src/models/category.dart"; +export "src/models/shopping_cart.dart"; + +/// interfaces +export "src/interfaces/shop_repository_interface.dart"; +export "src/interfaces/category_repository_interface.dart"; +export "src/interfaces/product_repository_interface.dart"; +export "src/interfaces/shopping_cart_repository_interface.dart"; + +/// local +export "src/local/local_shop_repository.dart"; +export "src/local/local_category_repository.dart"; +export "src/local/local_product_repository.dart"; +export "src/local/local_shopping_cart_repository.dart"; + +/// services +export "src/services/shopping_service.dart"; diff --git a/packages/shopping_repository_interface/lib/src/interfaces/category_repository_interface.dart b/packages/shopping_repository_interface/lib/src/interfaces/category_repository_interface.dart new file mode 100644 index 0000000..f04a3a8 --- /dev/null +++ b/packages/shopping_repository_interface/lib/src/interfaces/category_repository_interface.dart @@ -0,0 +1,16 @@ +import "package:shopping_repository_interface/src/models/category.dart"; + +/// The category repository interface. +abstract class CategoryRepositoryInterface { + /// Get the categories. + Stream> getCategories(); + + /// Select a category. + Category? selectCategory(String? categoryId); + + /// Get the selected category stream. + Stream?> getSelectedCategoryStream(); + + /// Deselect a category. + void deselectCategory(String? categoryId); +} diff --git a/packages/shopping_repository_interface/lib/src/interfaces/product_repository_interface.dart b/packages/shopping_repository_interface/lib/src/interfaces/product_repository_interface.dart new file mode 100644 index 0000000..dc96a65 --- /dev/null +++ b/packages/shopping_repository_interface/lib/src/interfaces/product_repository_interface.dart @@ -0,0 +1,16 @@ +import "package:shopping_repository_interface/shopping_repository_interface.dart"; + +/// The product repository interface. +abstract class ProductRepositoryInterface { + /// Get the products. + Stream> getProducts(List? categories, String shopId); + + /// Get the product stream. + Product? selectProduct(String? productId); + + /// Get the product. + Product? getProduct(String? productId); + + /// Get the weekly offer. + Product? getWeeklyOffer(); +} diff --git a/packages/shopping_repository_interface/lib/src/interfaces/shop_repository_interface.dart b/packages/shopping_repository_interface/lib/src/interfaces/shop_repository_interface.dart new file mode 100644 index 0000000..35a48ff --- /dev/null +++ b/packages/shopping_repository_interface/lib/src/interfaces/shop_repository_interface.dart @@ -0,0 +1,16 @@ +import "package:shopping_repository_interface/src/models/shop.dart"; + +/// The shop repository interface. +abstract class ShopRepositoryInterface { + /// Get the shops. + Stream> getShops(); + + /// Select a shop. + Shop? selectShop(String? shopId); + + /// Get the selected shop. + Shop? getSelectedShop(); + + /// Get the shop. + Shop? getShop(String? shopId); +} diff --git a/packages/shopping_repository_interface/lib/src/interfaces/shopping_cart_repository_interface.dart b/packages/shopping_repository_interface/lib/src/interfaces/shopping_cart_repository_interface.dart new file mode 100644 index 0000000..5b845ae --- /dev/null +++ b/packages/shopping_repository_interface/lib/src/interfaces/shopping_cart_repository_interface.dart @@ -0,0 +1,16 @@ +import "package:shopping_repository_interface/shopping_repository_interface.dart"; + +/// The shopping cart repository interface. +abstract class ShoppingCartRepositoryInterface { + /// Get the cart length. + Stream getCartLength(); + + /// Get the shopping cart. + Stream getShoppingCart(); + + /// Add a product to the cart. + Future addProductToCart(Product product); + + /// Remove a product from the cart. + Future removeProductFromCart(Product product); +} diff --git a/packages/shopping_repository_interface/lib/src/local/local_category_repository.dart b/packages/shopping_repository_interface/lib/src/local/local_category_repository.dart new file mode 100644 index 0000000..70e5686 --- /dev/null +++ b/packages/shopping_repository_interface/lib/src/local/local_category_repository.dart @@ -0,0 +1,51 @@ +import "dart:async"; + +import "package:collection/collection.dart"; +import "package:rxdart/rxdart.dart"; +import "package:shopping_repository_interface/src/interfaces/category_repository_interface.dart"; +import "package:shopping_repository_interface/src/models/category.dart"; + +/// The local category repository. +class LocalCategoryRepository implements CategoryRepositoryInterface { + final StreamController> _categoryController = + BehaviorSubject>(); + + final StreamController> _selectedCategoriesController = + BehaviorSubject>(); + + final _categories = [ + const Category(id: "1", name: "Bread"), + const Category(id: "2", name: "Cheese"), + const Category(id: "3", name: "Drinks"), + ]; + + final List _selectedCategories = []; + + @override + Stream> getCategories() { + _categoryController.add(_categories); + return _categoryController.stream; + } + + @override + Stream?> getSelectedCategoryStream() { + _selectedCategoriesController.add(_selectedCategories); + return _selectedCategoriesController.stream; + } + + @override + Category? selectCategory(String? categoryId) { + var selectedCategory = + _categories.firstWhereOrNull((category) => category.id == categoryId); + if (selectedCategory == null) return null; + _selectedCategories.add(selectedCategory); + _selectedCategoriesController.add(_selectedCategories); + return selectedCategory; + } + + @override + void deselectCategory(String? categoryId) { + _selectedCategories.removeWhere((category) => category.id == categoryId); + _selectedCategoriesController.add(_selectedCategories); + } +} diff --git a/packages/shopping_repository_interface/lib/src/local/local_product_repository.dart b/packages/shopping_repository_interface/lib/src/local/local_product_repository.dart new file mode 100644 index 0000000..d017728 --- /dev/null +++ b/packages/shopping_repository_interface/lib/src/local/local_product_repository.dart @@ -0,0 +1,165 @@ +import "dart:async"; +import "package:collection/collection.dart"; +import "package:rxdart/rxdart.dart"; +import "package:shopping_repository_interface/shopping_repository_interface.dart"; + +/// The local product repository. +class LocalProductRepository implements ProductRepositoryInterface { + /// Shop one products. + final shopOne = [ + const Product( + id: "1", + name: "White bread", + price: 2.50, + description: + "White bread is a common type of bread made from ground wheat" + " flour from which the bran and germ have been removed," + " giving it a light color.", + discountPrice: 1.50, + imageUrl: "https://shorturl.at/qeY8a", + category: "Bread", + isDiscounted: false, + ), + const Product( + id: "2", + name: "Brown bread", + price: 2.50, + description: + "Brown bread is a common type of bread made from ground wheat" + " flour from which the bran and germ have been removed," + " giving it a light color.", + discountPrice: 1.50, + imageUrl: "https://shorturl.at/qeY8a", + category: "Bread", + isDiscounted: true, + ), + const Product( + id: "3", + name: "Corn bread", + price: 2.50, + description: "Corn bread is a common type of bread made from ground wheat" + " flour from which the bran and germ have been removed," + " giving it a light color.", + discountPrice: 1.50, + imageUrl: "https://shorturl.at/qeY8a", + category: "Bread", + isDiscounted: false, + ), + ]; + + /// Shop two products. + final shopTwo = [ + const Product( + id: "4", + name: "cow milk", + price: 1.50, + description: "Cow milk is a common type of milk, it comes from cows", + discountPrice: 1.00, + imageUrl: "https://shorturl.at/RESqM", + category: "Drinks", + isDiscounted: true, + ), + const Product( + id: "5", + name: "goat milk", + price: 1.50, + description: "Goat milk is a common type of milk, it comes from goats", + discountPrice: 1.00, + imageUrl: "https://shorturl.at/RESqM", + category: "Drinks", + isDiscounted: true, + ), + const Product( + id: "6", + name: "sheep milk", + price: 1.50, + description: "Sheep milk is a common type of milk, it comes from sheeps", + discountPrice: 1.00, + imageUrl: "https://shorturl.at/RESqM", + category: "Drinks", + isDiscounted: true, + ), + ]; + + /// Shop three products. + final shopThree = [ + const Product( + id: "7", + name: "young cheese", + price: 3.50, + description: "Young cheese is a common type of cheese, it is very nice", + discountPrice: 2.50, + imageUrl: "https://shorturl.at/5QMoa", + category: "Cheese", + isDiscounted: false, + ), + const Product( + id: "8", + name: "old cheese", + price: 3.50, + description: "Old cheese is a common type of cheese, it is very nice", + discountPrice: 2.50, + imageUrl: "https://shorturl.at/5QMoa", + category: "Cheese", + isDiscounted: false, + ), + const Product( + id: "9", + name: "blue cheese", + price: 3.50, + description: "Blue cheese is a common type of cheese, it is very nice", + discountPrice: 2.50, + imageUrl: "https://shorturl.at/5QMoa", + category: "Cheese", + isDiscounted: true, + ), + ]; + + final List _products = []; + + Product? _selectedProduct; + + final StreamController> _productStream = + BehaviorSubject>(); + + @override + Product? getProduct(String? productId) => + _products.firstWhere((product) => product.id == productId); + + @override + Stream> getProducts(List? categories, String shopId) { + _products.clear(); + var products = shopId == "1" + ? shopOne + : shopId == "2" + ? shopTwo + : shopThree; + if (categories != null && categories.isNotEmpty) { + _products.addAll( + products + .where( + (product) => categories + .any((category) => category.name == product.category), + ) + .toList(), + ); + } else { + _products.addAll(products); + } + + _productStream.add(_products); + + return _productStream.stream; + } + + @override + Product? selectProduct(String? productId) { + _selectedProduct = + _products.firstWhere((product) => product.id == productId); + return _selectedProduct; + } + + @override + Product? getWeeklyOffer() => + _products.firstWhereOrNull((product) => product.isDiscounted); +} diff --git a/packages/shopping_repository_interface/lib/src/local/local_shop_repository.dart b/packages/shopping_repository_interface/lib/src/local/local_shop_repository.dart new file mode 100644 index 0000000..87fc527 --- /dev/null +++ b/packages/shopping_repository_interface/lib/src/local/local_shop_repository.dart @@ -0,0 +1,29 @@ +import "package:shopping_repository_interface/src/interfaces/shop_repository_interface.dart"; +import "package:shopping_repository_interface/src/models/shop.dart"; + +/// The local shop repository. +class LocalShopRepository implements ShopRepositoryInterface { + final List _shops = [ + const Shop(id: "1", name: "Bakkerij de Goudkorst", adress: "Bakkerssteeg"), + const Shop(id: "2", name: "De Verse Melkkan", adress: "Melkweg"), + const Shop(id: "3", name: "De Gouden Kaasplank", adress: "Kaashof"), + ]; + + Shop? _selectedShop; + + @override + Stream> getShops() => Stream.value(_shops); + + @override + Shop? getShop(String? shopId) => + _shops.firstWhere((shop) => shop.id == shopId); + + @override + Shop? getSelectedShop() => _selectedShop; + + @override + Shop? selectShop(String? shopId) { + _selectedShop = _shops.firstWhere((shop) => shop.id == shopId); + return _selectedShop; + } +} diff --git a/packages/shopping_repository_interface/lib/src/local/local_shopping_cart_repository.dart b/packages/shopping_repository_interface/lib/src/local/local_shopping_cart_repository.dart new file mode 100644 index 0000000..d43ac50 --- /dev/null +++ b/packages/shopping_repository_interface/lib/src/local/local_shopping_cart_repository.dart @@ -0,0 +1,76 @@ +import "dart:async"; + +import "package:rxdart/rxdart.dart"; +import "package:shopping_repository_interface/shopping_repository_interface.dart"; + +/// The local shopping cart repository. +class LocalShoppingCartRepository implements ShoppingCartRepositoryInterface { + /// The shopping cart. + ShoppingCart shoppingCart = const ShoppingCart( + id: "1", + products: [], + totalAmount: 0.0, + totalAmountWithDiscount: 0.0, + ); + + final StreamController _shoppingCartController = + BehaviorSubject(); + + @override + Future addProductToCart(Product product) async { + var existingProducts = shoppingCart.products; + var index = existingProducts.indexWhere((p) => p.id == product.id); + + if (index != -1) { + existingProducts[index] = product.copyWith( + selectedAmount: existingProducts[index].selectedAmount + 1, + ); + shoppingCart = shoppingCart.copyWith( + products: existingProducts, + totalAmount: shoppingCart.totalAmount + product.price, + totalAmountWithDiscount: product.isDiscounted + ? shoppingCart.totalAmountWithDiscount + product.discountPrice + : shoppingCart.totalAmountWithDiscount + product.price, + ); + } else { + shoppingCart = shoppingCart.copyWith( + products: [...existingProducts, product.copyWith(selectedAmount: 1)], + totalAmount: shoppingCart.totalAmount + product.price, + totalAmountWithDiscount: product.isDiscounted + ? shoppingCart.totalAmountWithDiscount + product.discountPrice + : shoppingCart.totalAmountWithDiscount + product.price, + ); + } + + _shoppingCartController.add(shoppingCart); + } + + @override + Stream getCartLength() => + _shoppingCartController.stream.map((cart) => cart.products.length); + + @override + Stream getShoppingCart() => _shoppingCartController.stream; + + @override + Future removeProductFromCart(Product product) async { + var existingProducts = shoppingCart.products; + if (existingProducts.contains(product)) { + var index = existingProducts.indexOf(product); + if (product.selectedAmount == 1) { + existingProducts.removeAt(index); + } else { + existingProducts[index] = + product.copyWith(selectedAmount: product.selectedAmount - 1); + } + } + shoppingCart = shoppingCart.copyWith( + products: existingProducts, + totalAmount: shoppingCart.totalAmount + product.price, + totalAmountWithDiscount: product.isDiscounted + ? shoppingCart.totalAmountWithDiscount - product.discountPrice + : shoppingCart.totalAmountWithDiscount - product.price, + ); + _shoppingCartController.add(shoppingCart); + } +} diff --git a/packages/shopping_repository_interface/lib/src/models/category.dart b/packages/shopping_repository_interface/lib/src/models/category.dart new file mode 100644 index 0000000..d427174 --- /dev/null +++ b/packages/shopping_repository_interface/lib/src/models/category.dart @@ -0,0 +1,26 @@ +/// Category model +class Category { + /// Constructor for the category. + const Category({ + required this.id, + required this.name, + }); + + /// Create a category from a map. + factory Category.fromMap(String id, Map map) => Category( + id: id, + name: map["name"], + ); + + /// The id of the category. + final String id; + + /// The name of the category. + final String name; + + /// Convert the category to a map. + Map toMap() => { + "id": id, + "name": name, + }; +} diff --git a/packages/shopping_repository_interface/lib/src/models/product.dart b/packages/shopping_repository_interface/lib/src/models/product.dart new file mode 100644 index 0000000..0126e71 --- /dev/null +++ b/packages/shopping_repository_interface/lib/src/models/product.dart @@ -0,0 +1,80 @@ +/// A model class representing a product. +class Product { + /// Constructor for the product. + const Product({ + required this.id, + required this.name, + required this.description, + required this.price, + required this.discountPrice, + required this.imageUrl, + required this.category, + this.isDiscounted = false, + this.selectedAmount = 0, + }); + + /// Create a product from a map. + factory Product.fromMap(Map map) => Product( + id: map["id"], + name: map["name"], + description: map["description"], + price: map["price"], + // ignore: avoid_dynamic_calls + discountPrice: map["discountPrice"].toDouble(), + imageUrl: map["imageUrl"], + category: map["category"], + isDiscounted: map["isDiscounted"], + selectedAmount: map["selectedAmount"] ?? 0, + ); + + /// The id of the product. + final String id; + + /// The name of the product. + final String name; + + /// The description of the product. + final String description; + + /// The price of the product. + final double price; + + /// The discount price of the product. + final double discountPrice; + + /// The image url of the product. + final String imageUrl; + + /// The category of the product. + final String category; + + /// Whether the product is discounted. + final bool isDiscounted; + + /// The selected amount of the product. + final int selectedAmount; + + /// Copy the product with new values. + Product copyWith({ + String? id, + String? name, + String? description, + double? price, + double? discountPrice, + String? imageUrl, + String? category, + bool? isDiscounted, + int? selectedAmount, + }) => + Product( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + price: price ?? this.price, + discountPrice: discountPrice ?? this.discountPrice, + imageUrl: imageUrl ?? this.imageUrl, + category: category ?? this.category, + isDiscounted: isDiscounted ?? this.isDiscounted, + selectedAmount: selectedAmount ?? this.selectedAmount, + ); +} diff --git a/packages/shopping_repository_interface/lib/src/models/shop.dart b/packages/shopping_repository_interface/lib/src/models/shop.dart new file mode 100644 index 0000000..703a60c --- /dev/null +++ b/packages/shopping_repository_interface/lib/src/models/shop.dart @@ -0,0 +1,32 @@ +/// Shop model +class Shop { + /// Constructor for the shop. + const Shop({ + required this.id, + required this.name, + required this.adress, + }); + + /// Create a shop from a map. + factory Shop.fromMap(String id, Map map) => Shop( + id: id, + name: map["name"], + adress: map["adress"], + ); + + /// The id of the shop. + final String id; + + /// The name of the shop. + final String name; + + /// The adress of the shop. + final String adress; + + /// Convert the shop to a map. + Map toMap() => { + "id": id, + "name": name, + "adress": adress, + }; +} diff --git a/packages/shopping_repository_interface/lib/src/models/shopping_cart.dart b/packages/shopping_repository_interface/lib/src/models/shopping_cart.dart new file mode 100644 index 0000000..210f22f --- /dev/null +++ b/packages/shopping_repository_interface/lib/src/models/shopping_cart.dart @@ -0,0 +1,39 @@ +import "package:shopping_repository_interface/shopping_repository_interface.dart"; + +/// The shopping cart. +class ShoppingCart { + /// The shopping cart. + const ShoppingCart({ + required this.id, + required this.products, + this.totalAmount = 0.00, + this.totalAmountWithDiscount = 0.0, + }); + + /// The id. + final String id; + + /// The products. + final List products; + + /// The total amount. + final double totalAmount; + + /// The total amount with discount. + final double totalAmountWithDiscount; + + /// Copy the shopping cart with new values. + ShoppingCart copyWith({ + String? id, + List? products, + double? totalAmount, + double? totalAmountWithDiscount, + }) => + ShoppingCart( + id: id ?? this.id, + products: products ?? this.products, + totalAmount: totalAmount ?? this.totalAmount, + totalAmountWithDiscount: + totalAmountWithDiscount ?? this.totalAmountWithDiscount, + ); +} diff --git a/packages/shopping_repository_interface/lib/src/services/shopping_service.dart b/packages/shopping_repository_interface/lib/src/services/shopping_service.dart new file mode 100644 index 0000000..d2d1440 --- /dev/null +++ b/packages/shopping_repository_interface/lib/src/services/shopping_service.dart @@ -0,0 +1,104 @@ +import "package:shopping_repository_interface/shopping_repository_interface.dart"; + +/// The shopping service. +class ShoppingService { + /// The Construtor of the shopping service. + ShoppingService({ + ShopRepositoryInterface? shopRepository, + ProductRepositoryInterface? productRepository, + CategoryRepositoryInterface? categoryRepository, + ShoppingCartRepositoryInterface? shoppingCartRepository, + }) : shopRepository = shopRepository ?? LocalShopRepository(), + productRepository = productRepository ?? LocalProductRepository(), + categoryRepository = categoryRepository ?? LocalCategoryRepository(), + shoppingCartRepository = + shoppingCartRepository ?? LocalShoppingCartRepository(); + + /// The shop repository. + final ShopRepositoryInterface shopRepository; + + /// The product repository. + final ProductRepositoryInterface productRepository; + + /// The category repository. + final CategoryRepositoryInterface categoryRepository; + + /// The shopping cart repository. + final ShoppingCartRepositoryInterface shoppingCartRepository; + + /// The selected categories. + final List selectedCategories = []; + + /// The selected shop. + Shop? selectedShop; + + /// Get the shops. + Stream> getShops() => shopRepository.getShops(); + + /// Select a shop. + Shop? selectShop(String shopId) { + selectedShop = shopRepository.selectShop(shopId); + return selectedShop; + } + + /// Get the selected shop. + Shop? getSelectedShop() => shopRepository.getSelectedShop(); + + /// Get the shop. + Shop? getShop(String? shopId) => shopRepository.getShop(shopId); + + /// Get the products. + Stream> getProducts(String shopId) => + productRepository.getProducts( + selectedCategories, + shopId, + ); + + /// Select a product. + Product? selectProduct(String? productId) => + productRepository.selectProduct(productId); + + /// Get the product. + Product? getProduct(String? productId) => + productRepository.getProduct(productId); + + /// Get the weekly offer. + Product? getWeeklyOffer() => productRepository.getWeeklyOffer(); + + /// Get the categories. + Stream> getCategories() => categoryRepository.getCategories(); + + /// Select a category. + Category? selectCategory(String? categoryId) { + var category = categoryRepository.selectCategory(categoryId); + if (category != null) selectedCategories.add(category); + getProducts(selectedShop?.id ?? ""); + return category; + } + + /// Get the selected category stream. + Stream?> getSelectedCategoryStream() => + categoryRepository.getSelectedCategoryStream(); + + /// Deselect a category. + void deselectCategory(String? categoryId) { + selectedCategories.removeWhere((category) => category.id == categoryId); + getProducts(selectedShop?.id ?? ""); + categoryRepository.deselectCategory(categoryId); + } + + /// Get the cart length. + Stream getCartLength() => shoppingCartRepository.getCartLength(); + + /// Get the shopping cart. + Stream getShoppingCart() => + shoppingCartRepository.getShoppingCart(); + + /// Add a product to the cart. + Future addProductToCart(Product product) => + shoppingCartRepository.addProductToCart(product); + + /// Remove a product from the cart. + Future removeProductFromCart(Product product) => + shoppingCartRepository.removeProductFromCart(product); +} diff --git a/packages/shopping_repository_interface/pubspec.yaml b/packages/shopping_repository_interface/pubspec.yaml new file mode 100644 index 0000000..c19a44d --- /dev/null +++ b/packages/shopping_repository_interface/pubspec.yaml @@ -0,0 +1,21 @@ +name: shopping_repository_interface +description: "A new Flutter package project." +version: 3.0.0 +homepage: https://github.com/Iconica-Development/flutter_shopping/packages/shopping_repository_interface +publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/ + +environment: + sdk: ^3.5.1 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + collection: ^1.18.0 + rxdart: ^0.28.0 + +dev_dependencies: + flutter_iconica_analysis: + git: + url: https://github.com/Iconica-Development/flutter_iconica_analysis + ref: 7.0.0