diff --git a/packages/flutter_order_details/lib/flutter_order_details.dart b/packages/flutter_order_details/lib/flutter_order_details.dart index 9bc1b8b..d22fb55 100644 --- a/packages/flutter_order_details/lib/flutter_order_details.dart +++ b/packages/flutter_order_details/lib/flutter_order_details.dart @@ -1,8 +1,9 @@ /// 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_localization.dart"; -export "src/configuration/order_detail_title_style.dart"; -export "src/models/order_result.dart"; -export "src/widgets/order_detail_screen.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 index ab0d019..def8d8c 100644 --- a/packages/flutter_order_details/lib/src/configuration/order_detail_configuration.dart +++ b/packages/flutter_order_details/lib/src/configuration/order_detail_configuration.dart @@ -1,537 +1,71 @@ -// ignore_for_file: avoid_annotating_with_dynamic - -import "package:animated_toggle/animated_toggle.dart"; import "package:flutter/material.dart"; -import "package:flutter_form_wizard/flutter_form.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. - OrderDetailConfiguration({ - required this.onCompleted, - this.pages = _defaultPages, - this.localization = const OrderDetailLocalization(), - this.appBar = _defaultAppBar, - this.nextbuttonBuilder = _defaultNextButtonBuilder, + 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; + 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(dynamic value) onCompleted; + 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 OrderDetailLocalization localization; + final OrderDetailTranslations translations; /// Optional app bar that you can pass to the order detail screen. - final AppBar Function( - BuildContext context, - OrderDetailLocalization localizations, - ) appBar; + 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 a, + int currentStep, // ignore: avoid_positional_boolean_parameters - bool b, + bool checkingPages, BuildContext context, OrderDetailConfiguration configuration, FlutterFormController controller, - ) nextbuttonBuilder; -} - -AppBar _defaultAppBar( - BuildContext context, - OrderDetailLocalization localizations, -) { - var theme = Theme.of(context); - return AppBar( - title: Text( - localizations.orderDetailsTitle, - style: theme.textTheme.headlineLarge, - ), - ); -} - -Widget _defaultNextButtonBuilder( - int currentStep, - bool b, - BuildContext context, - OrderDetailConfiguration configuration, - FlutterFormController controller, -) { - 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 { - controller.validateAndSaveCurrentStep(); - await controller.autoNextStep(); - }, - 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, - ), - ), - ), - ), - ), - ); -} - -List _defaultPages(BuildContext context) { - 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; - }, - childLeft: Center( - child: ListenableBuilder( - listenable: switchStatus, - builder: (context, widget) => Text( - "Morning", - style: theme.textTheme.titleSmall?.copyWith( - color: switchStatus.value - ? theme.colorScheme.primary - : Colors.white, - ), - ), - ), - ), - childRight: Center( - child: ListenableBuilder( - listenable: switchStatus, - builder: (context, widget) => Text( - "Afternoon", - style: theme.textTheme.titleSmall?.copyWith( - color: switchStatus.value - ? Colors.white - : theme.colorScheme.primary, - ), - ), - ), - ), - ), - ), - const SizedBox( - height: 8, - ), - ListenableBuilder( - listenable: switchStatus, - builder: (context, widget) => 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", - ), - ], - ), - ), - ), - ]; + )? 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_title_style.dart b/packages/flutter_order_details/lib/src/configuration/order_detail_title_style.dart deleted file mode 100644 index 8cc7896..0000000 --- a/packages/flutter_order_details/lib/src/configuration/order_detail_title_style.dart +++ /dev/null @@ -1,14 +0,0 @@ -/// An enum to define the style of the title in the order detail. -enum OrderDetailTitleStyle { - /// The title displayed as a textlabel above the field. - text, - - /// The title displayed as a label inside the field. - /// NOTE: Not all fields support this. Such as, but not limited to: - /// - Dropdown - /// - Time Picker - label, - - /// Does not display any form of title. - none, -} diff --git a/packages/flutter_order_details/lib/src/configuration/order_detail_localization.dart b/packages/flutter_order_details/lib/src/configuration/order_detail_translations.dart similarity index 86% rename from packages/flutter_order_details/lib/src/configuration/order_detail_localization.dart rename to packages/flutter_order_details/lib/src/configuration/order_detail_translations.dart index df8c48c..55a306a 100644 --- a/packages/flutter_order_details/lib/src/configuration/order_detail_localization.dart +++ b/packages/flutter_order_details/lib/src/configuration/order_detail_translations.dart @@ -1,7 +1,7 @@ /// Localizations for the order detail page. -class OrderDetailLocalization { +class OrderDetailTranslations { /// Constructor for the order detail localization. - const OrderDetailLocalization({ + const OrderDetailTranslations({ this.nextButton = "Order", this.completeButton = "Complete", this.orderDetailsTitle = "Information", diff --git a/packages/flutter_order_details/lib/src/models/order_result.dart b/packages/flutter_order_details/lib/src/models/order_result.dart deleted file mode 100644 index 20a4c00..0000000 --- a/packages/flutter_order_details/lib/src/models/order_result.dart +++ /dev/null @@ -1,14 +0,0 @@ -/// OrderResult model. -/// When an user completes the field and presses the complete button, -/// the `onComplete` method returns an instance of this class that contains -/// all the developer-specified `outputKey`s and the value that was provided -/// by the user. -class OrderResult { - /// Constructor of the order result class. - OrderResult({ - required this.order, - }); - - /// Map of `outputKey`s and their respected values. - final Map order; -} diff --git a/packages/flutter_order_details/lib/src/order_detail_screen.dart b/packages/flutter_order_details/lib/src/order_detail_screen.dart new file mode 100644 index 0000000..a87f354 --- /dev/null +++ b/packages/flutter_order_details/lib/src/order_detail_screen.dart @@ -0,0 +1,68 @@ +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 new file mode 100644 index 0000000..57945ea --- /dev/null +++ b/packages/flutter_order_details/lib/src/widgets/default_appbar.dart @@ -0,0 +1,27 @@ +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 new file mode 100644 index 0000000..290fb9d --- /dev/null +++ b/packages/flutter_order_details/lib/src/widgets/default_next_button.dart @@ -0,0 +1,69 @@ +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 new file mode 100644 index 0000000..f2ec7b4 --- /dev/null +++ b/packages/flutter_order_details/lib/src/widgets/default_order_detail_pages.dart @@ -0,0 +1,431 @@ +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_detail_screen.dart b/packages/flutter_order_details/lib/src/widgets/order_detail_screen.dart deleted file mode 100644 index d24cd2a..0000000 --- a/packages/flutter_order_details/lib/src/widgets/order_detail_screen.dart +++ /dev/null @@ -1,46 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_form_wizard/flutter_form.dart"; -import "package:flutter_order_details/flutter_order_details.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.appBar - .call(context, widget.configuration.localization), - body: FlutterForm( - formController: controller, - options: FlutterFormOptions( - nextButton: (a, b) => widget.configuration.nextbuttonBuilder( - a, - b, - context, - widget.configuration, - controller, - ), - pages: widget.configuration.pages.call(context), - onFinished: (data) { - widget.configuration.onCompleted.call(data); - }, - onNext: (step, data) {}, - ), - ), - ); - } -} diff --git a/packages/flutter_shopping/lib/src/widgets/default_order_succes_widget.dart b/packages/flutter_order_details/lib/src/widgets/order_succes.dart similarity index 74% rename from packages/flutter_shopping/lib/src/widgets/default_order_succes_widget.dart rename to packages/flutter_order_details/lib/src/widgets/order_succes.dart index d0b3cb2..6abfd00 100644 --- a/packages/flutter_shopping/lib/src/widgets/default_order_succes_widget.dart +++ b/packages/flutter_order_details/lib/src/widgets/order_succes.dart @@ -1,21 +1,31 @@ import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.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-story. - final FlutterShoppingConfiguration configuration; + /// 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( @@ -42,16 +52,19 @@ class DefaultOrderSucces extends StatelessWidget { height: 4, ), Text( - "Thank you Peter for your order!", + "Thank you ${orderDetails[0]!['name']} for your order!", style: theme.textTheme.bodyMedium, ), const SizedBox( height: 16, ), Text( - "The order was placed at Bakkerij de Goudkorst." + "The order was placed" + // ignore: lines_longer_than_80_chars + " at ${configuration.shoppingService.shopService.selectedShop?.name}." " You can pick this" - " up on Monday, February 7 at 1:00 PM.", + " up ${orderDetails[1]!['date']} at" + " ${orderDetails[1]!['multipleChoice']}.", style: theme.textTheme.bodyMedium, textAlign: TextAlign.center, ), @@ -84,9 +97,19 @@ class DefaultOrderSucces extends StatelessWidget { scrollDirection: Axis.horizontal, children: [ const SizedBox(width: 32), - _discount(context), - const SizedBox(width: 8), - _discount(context), + // _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), ], ), @@ -98,7 +121,8 @@ class DefaultOrderSucces extends StatelessWidget { width: double.infinity, child: FilledButton( onPressed: () async { - configuration.onCompleteUserStory.call(context); + configuration.onCompleteOrderDetails + .call(context, configuration); }, style: theme.filledButtonTheme.style?.copyWith( backgroundColor: WidgetStateProperty.all( @@ -125,7 +149,7 @@ class DefaultOrderSucces extends StatelessWidget { } } -Widget _discount(BuildContext context) { +Widget _discount(BuildContext context, Product product, Shop shop) { var theme = Theme.of(context); return Container( decoration: BoxDecoration( @@ -139,14 +163,11 @@ Widget _discount(BuildContext context) { child: Stack( children: [ ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(10), - topRight: Radius.circular(10), - bottomLeft: Radius.circular(10), - bottomRight: Radius.circular(10), + borderRadius: BorderRadius.circular( + 10, ), child: Image.network( - "https://picsum.photos/150", + product.imageUrl, width: double.infinity, height: double.infinity, fit: BoxFit.cover, @@ -166,7 +187,7 @@ Widget _discount(BuildContext context) { child: Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( - "Butcher Puurvlees", + shop.name, style: theme.textTheme.headlineSmall?.copyWith( color: Colors.white, ), @@ -189,7 +210,7 @@ Widget _discount(BuildContext context) { child: Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( - "Chicken legs, now for 4,99", + "${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 index 20721cb..5e39831 100644 --- a/packages/flutter_order_details/pubspec.yaml +++ b/packages/flutter_order_details/pubspec.yaml @@ -1,10 +1,10 @@ name: flutter_order_details description: "A Flutter module for order details." version: 2.0.0 -publish_to: 'none' +publish_to: "none" environment: - sdk: '>=3.3.0 <4.0.0' + sdk: ">=3.3.0 <4.0.0" dependencies: flutter: @@ -17,6 +17,12 @@ dependencies: 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: @@ -27,4 +33,3 @@ dev_dependencies: ref: 7.0.0 flutter: - diff --git a/packages/flutter_product_page/lib/flutter_product_page.dart b/packages/flutter_product_page/lib/flutter_product_page.dart index dbcd53a..f15fb25 100644 --- a/packages/flutter_product_page/lib/flutter_product_page.dart +++ b/packages/flutter_product_page/lib/flutter_product_page.dart @@ -2,11 +2,7 @@ /// detailed view of each product. library flutter_product_page; -export "src/configuration/product_page_category_styling_configuration.dart"; export "src/configuration/product_page_configuration.dart"; -export "src/configuration/product_page_content.dart"; -export "src/configuration/product_page_localization.dart"; export "src/configuration/product_page_shop_selector_style.dart"; -export "src/models/product_page_shop.dart"; -export "src/ui/product_page.dart"; -export "src/ui/product_page_screen.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 new file mode 100644 index 0000000..0f3649f --- /dev/null +++ b/packages/flutter_product_page/lib/src/category_selection_screen.dart @@ -0,0 +1,67 @@ +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_category_styling_configuration.dart b/packages/flutter_product_page/lib/src/configuration/product_page_category_styling_configuration.dart deleted file mode 100644 index f554222..0000000 --- a/packages/flutter_product_page/lib/src/configuration/product_page_category_styling_configuration.dart +++ /dev/null @@ -1,54 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_nested_categories/flutter_nested_categories.dart" - show CategoryHeaderStyling; - -/// Configuration for the styling of the category list on the product page. -/// This configuration allows to customize the title, header styling and -/// the collapsible behavior of the categories. -class ProductPageCategoryStylingConfiguration { - /// Constructor to create a new instance of - /// [ProductPageCategoryStylingConfiguration]. - const ProductPageCategoryStylingConfiguration({ - this.headerStyling, - this.headerCentered = false, - this.customTitle, - this.title, - this.titleStyle, - this.titleCentered = false, - this.isCategoryCollapsible = true, - }); - - /// Optional title for the category list. This will be displayed at the - /// top of the list. - final String? title; - - /// Optional custom title widget for the category list. This will be - /// displayed at the top of the list. If set, the text title will be - /// ignored. - final Widget? customTitle; - - /// Optional title style for the title of the category list. This will - /// be applied to the title of the category list. If not set, the default - /// text style will be used. - final TextStyle? titleStyle; - - /// Configure if the title should be centered. - /// - /// Default is false. - final bool titleCentered; - - /// Optional header styling for the categories. This will be applied to - /// the name of the categories. If not set, the default text style will - /// be used. - final CategoryHeaderStyling? headerStyling; - - /// Configure if the category header should be centered. - /// - /// Default is false. - final bool headerCentered; - - /// Configure if the category should be collapsible. - /// - /// Default is true. - final bool isCategoryCollapsible; -} 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 index d3f440c..b9ef07a 100644 --- a/packages/flutter_product_page/lib/src/configuration/product_page_configuration.dart +++ b/packages/flutter_product_page/lib/src/configuration/product_page_configuration.dart @@ -1,165 +1,84 @@ import "package:flutter/material.dart"; -import "package:flutter_product_page/src/services/shopping_cart_notifier.dart"; -import "package:flutter_product_page/src/ui/widgets/product_item_popup.dart"; -import "package:flutter_shopping/flutter_shopping.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, - this.navigateToShoppingCartBuilder = _defaultNavigateToShoppingCartBuilder, + required this.getProductsInShoppingCart, + this.shoppingCartButtonBuilder, this.initialShopId, this.productBuilder, this.onShopSelectionChange, - this.getProductsInShoppingCart, - this.localizations = const ProductPageLocalization(), - this.shopSelectorStyle = ShopSelectorStyle.spacedWrap, - this.categoryStylingConfiguration = - const ProductPageCategoryStylingConfiguration(), + this.translations = const ProductPageTranslations(), + this.shopSelectorStyle = ShopSelectorStyle.row, this.pagePadding = const EdgeInsets.all(4), - this.appBar = _defaultAppBar, + this.appBarBuilder, this.bottomNavigationBar, - Function( - BuildContext context, - Product product, - )? onProductDetail, - String Function( - Product product, - )? getDiscountDescription, - Widget Function( - BuildContext context, - Product product, - )? productPopupBuilder, - Widget Function( - BuildContext context, - )? noContentBuilder, - Widget Function( - BuildContext context, - Object? error, - StackTrace? stackTrace, - )? errorBuilder, + this.onProductDetail, + this.discountDescription, + this.noContentBuilder, + this.errorBuilder, + this.shopselectorBuilder, + this.discountBuilder, + this.categoryListBuilder, + this.selectedCategoryBuilder, }) { - _productPopupBuilder = productPopupBuilder; - _productPopupBuilder ??= - (BuildContext context, Product product) => ProductItemPopup( - product: product, - configuration: this, - ); - - _onProductDetail = onProductDetail; - _onProductDetail ??= (BuildContext context, Product product) async { - var theme = Theme.of(context); - - await showModalBottomSheet( - context: context, - backgroundColor: theme.colorScheme.surface, - builder: (context) => _productPopupBuilder!( - context, - product, - ), - ); - }; - - _noContentBuilder = noContentBuilder; - _noContentBuilder ??= (BuildContext context) { - var theme = Theme.of(context); - return Center( - child: Text( - "No content", - style: theme.textTheme.titleLarge, - ), - ); - }; - - _errorBuilder = errorBuilder; - _errorBuilder ??= - (BuildContext context, Object? error, StackTrace? stackTrace) { - var theme = Theme.of(context); - return Center( - child: Text( - "Error: $error", - style: theme.textTheme.titleLarge, - ), - ); - }; - - _getDiscountDescription = getDiscountDescription; - _getDiscountDescription ??= (Product product) => - "${product.name}, now for ${product.discountPrice} each"; + 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> shops; + final Future> Function() shops; /// A function that returns all the products that belong to a certain shop. - /// The function must return a [ProductPageContent] object. - final Future Function(ProductPageShop shop) getProducts; + /// The function must return a [List]. + final Future> Function(Shop shop) getProducts; /// The localizations for the product page. - final ProductPageLocalization localizations; + 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. - Widget Function(BuildContext context, Product product)? productBuilder; + final Widget Function( + BuildContext context, + Product product, + ProductPageConfiguration configuration, + )? productBuilder; - late Widget Function(BuildContext context, Product product)? - _productPopupBuilder; - - /// The builder for the product popup. This popup will be displayed when the - /// user clicks on a product. This builder should only build the widget that - /// displays the content of one specific product. - /// This builder has a default in-case the developer - Widget Function(BuildContext context, Product product) - get productPopupBuilder => _productPopupBuilder!; - - late Function(BuildContext context, Product product)? _onProductDetail; - - /// This function handles the creation of the product detail popup. This - /// function has a default in-case the developer does not override it. - /// The default intraction is a popup, but this can be overriden. - Function(BuildContext context, Product product) get onProductDetail => - _onProductDetail!; - - late Widget Function(BuildContext context)? _noContentBuilder; - - /// The no content builder is used when a shop has no products. This builder - /// has a default in-case the developer does not override it. - Function(BuildContext context)? get noContentBuilder => _noContentBuilder; + /// 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. - Widget Function( + final Widget Function( BuildContext context, ProductPageConfiguration configuration, - ShoppingCartNotifier notifier, - ) navigateToShoppingCartBuilder; + )? shoppingCartButtonBuilder; - late Widget Function( - BuildContext context, - Object? error, - StackTrace? stackTrace, - )? _errorBuilder; - - /// The error builder is used when an error occurs. This builder has a default - /// in-case the developer does not override it. - Widget Function(BuildContext context, Object? error, StackTrace? stackTrace)? - get errorBuilder => _errorBuilder; - - late String Function(Product product)? _getDiscountDescription; - - /// The function that returns the description of the discount for a product. - /// This allows you to translate and give custom messages for each product. - String Function(Product product)? get getDiscountDescription => - _getDiscountDescription!; + /// 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. @@ -167,11 +86,11 @@ class ProductPageConfiguration { /// 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(ProductPageShop shop)? onShopSelectionChange; + 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; + final int Function() getProductsInShoppingCart; /// This function must be implemented by the developer and should handle the /// navigation to the shopping cart overview page. @@ -180,9 +99,6 @@ class ProductPageConfiguration { /// The style of the shop selector. final ShopSelectorStyle shopSelectorStyle; - /// The styling configuration for the category list. - final ProductPageCategoryStylingConfiguration categoryStylingConfiguration; - /// The padding for the page. final EdgeInsets pagePadding; @@ -190,60 +106,68 @@ class ProductPageConfiguration { final Widget? bottomNavigationBar; /// Optional app bar that you can pass to the order detail screen. - final AppBar Function(BuildContext context)? appBar; + 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; } -AppBar _defaultAppBar( +Future _onProductDetail( BuildContext context, -) { + Product product, + String closeText, +) async { var theme = Theme.of(context); - return AppBar( - leading: IconButton(onPressed: () {}, icon: const Icon(Icons.person)), - actions: [ - IconButton(onPressed: () {}, icon: const Icon(Icons.filter_alt)), - ], - title: Text( - "Product page", - style: theme.textTheme.headlineLarge, + await showModalBottomSheet( + context: context, + backgroundColor: theme.colorScheme.surface, + builder: (context) => ProductItemPopup( + product: product, + closeText: closeText, ), ); } -Widget _defaultNavigateToShoppingCartBuilder( - BuildContext context, - ProductPageConfiguration configuration, - ShoppingCartNotifier notifier, -) { - var theme = Theme.of(context); - - return ListenableBuilder( - listenable: notifier, - builder: (context, widget) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 60), - child: SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: configuration.getProductsInShoppingCart?.call() != 0 - ? 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( - configuration.localizations.navigateToShoppingCart, - style: theme.textTheme.displayLarge, - ), - ), - ), - ), - ), - ); -} +String _defaultDiscountDescription( + Product product, +) => + "${product.name}, now for ${product.discountPrice} each"; diff --git a/packages/flutter_product_page/lib/src/configuration/product_page_content.dart b/packages/flutter_product_page/lib/src/configuration/product_page_content.dart deleted file mode 100644 index 018d822..0000000 --- a/packages/flutter_product_page/lib/src/configuration/product_page_content.dart +++ /dev/null @@ -1,16 +0,0 @@ -import "package:flutter_shopping/flutter_shopping.dart"; - -/// Return type that contains the products and an optional discounted product. -class ProductPageContent { - /// Default constructor for this class. - const ProductPageContent({ - required this.products, - this.discountedProduct, - }); - - /// List of products that belong to the shop. - final List products; - - /// Optional highlighted discounted product to display. - final Product? discountedProduct; -} diff --git a/packages/flutter_product_page/lib/src/configuration/product_page_localization.dart b/packages/flutter_product_page/lib/src/configuration/product_page_translations.dart similarity index 64% rename from packages/flutter_product_page/lib/src/configuration/product_page_localization.dart rename to packages/flutter_product_page/lib/src/configuration/product_page_translations.dart index f2bbe3f..ff6bfd2 100644 --- a/packages/flutter_product_page/lib/src/configuration/product_page_localization.dart +++ b/packages/flutter_product_page/lib/src/configuration/product_page_translations.dart @@ -1,11 +1,13 @@ /// Localization for the product page -class ProductPageLocalization { +class ProductPageTranslations { /// Default constructor - const ProductPageLocalization({ + 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 @@ -19,4 +21,10 @@ class ProductPageLocalization { /// 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/models/product_page_shop.dart b/packages/flutter_product_page/lib/src/models/product_page_shop.dart deleted file mode 100644 index a4d0aa9..0000000 --- a/packages/flutter_product_page/lib/src/models/product_page_shop.dart +++ /dev/null @@ -1,18 +0,0 @@ -/// The product page shop class contains all the required information -/// that needs to be known about a certain shop. -/// -/// In your own implemententation, you must extend from this class so you can -/// add more fields to this class to suit your needs. -class ProductPageShop { - /// The default constructor for this class. - const ProductPageShop({ - required this.id, - required this.name, - }); - - /// The unique identifier for the shop. - final String id; - - /// The name of the shop. - final String name; -} diff --git a/packages/flutter_product_page/lib/src/product_page_screen.dart b/packages/flutter_product_page/lib/src/product_page_screen.dart new file mode 100644 index 0000000..6cc5303 --- /dev/null +++ b/packages/flutter_product_page/lib/src/product_page_screen.dart @@ -0,0 +1,306 @@ +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 index 5d39843..4081d53 100644 --- a/packages/flutter_product_page/lib/src/services/category_service.dart +++ b/packages/flutter_product_page/lib/src/services/category_service.dart @@ -1,28 +1,14 @@ import "package:flutter/material.dart"; import "package:flutter_nested_categories/flutter_nested_categories.dart"; -import "package:flutter_product_page/src/services/shopping_cart_notifier.dart"; -import "package:flutter_product_page/src/ui/components/product_item.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; - -/// A function that is called when a product is added to the cart. -Product onAddToCartWrapper( - ProductPageConfiguration configuration, - ShoppingCartNotifier shoppingCartNotifier, - Product product, -) { - shoppingCartNotifier.productsChanged(); - - configuration.onAddToCart(product); - - return product; -} +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, - ShoppingCartNotifier shoppingCartNotifier, List products, ) { var theme = Theme.of(context); @@ -39,18 +25,15 @@ Widget getCategoryList( categorizedProducts.forEach((categoryName, productList) { var productWidgets = productList .map( - (product) => configuration.productBuilder != null - ? configuration.productBuilder!(context, product) - : ProductItem( - product: product, - onProductDetail: configuration.onProductDetail, - onAddToCart: (Product product) => onAddToCartWrapper( - configuration, - shoppingCartNotifier, - product, - ), - localizations: configuration.localizations, - ), + (product) => + configuration.productBuilder + ?.call(context, product, configuration) ?? + DefaultProductItem( + product: product, + onAddToCart: configuration.onAddToCart, + onProductDetail: configuration.onProductDetail!, + translations: configuration.translations, + ), ) .toList(); var category = Category( diff --git a/packages/flutter_product_page/lib/src/services/selected_shop_service.dart b/packages/flutter_product_page/lib/src/services/selected_shop_service.dart deleted file mode 100644 index 502eaa5..0000000 --- a/packages/flutter_product_page/lib/src/services/selected_shop_service.dart +++ /dev/null @@ -1,21 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_product_page/src/models/product_page_shop.dart"; - -/// A service that provides the currently selected shop. -class SelectedShopService extends ChangeNotifier { - /// Creates a [SelectedShopService]. - SelectedShopService(); - - ProductPageShop? _selectedShop; - - /// Updates the selected shop. - void selectShop(ProductPageShop shop) { - if (_selectedShop == shop) return; - - _selectedShop = shop; - notifyListeners(); - } - - /// The currently selected shop. - ProductPageShop? get selectedShop => _selectedShop; -} diff --git a/packages/flutter_product_page/lib/src/services/shopping_cart_notifier.dart b/packages/flutter_product_page/lib/src/services/shopping_cart_notifier.dart deleted file mode 100644 index d02251f..0000000 --- a/packages/flutter_product_page/lib/src/services/shopping_cart_notifier.dart +++ /dev/null @@ -1,10 +0,0 @@ -import "package:flutter/material.dart"; - -/// Class that notifies listeners when the products in the shopping cart have -/// changed. -class ShoppingCartNotifier extends ChangeNotifier { - /// Notifies listeners that the products in the shopping cart have changed. - void productsChanged() { - notifyListeners(); - } -} diff --git a/packages/flutter_product_page/lib/src/ui/components/shop_selector.dart b/packages/flutter_product_page/lib/src/ui/components/shop_selector.dart deleted file mode 100644 index c9b3b76..0000000 --- a/packages/flutter_product_page/lib/src/ui/components/shop_selector.dart +++ /dev/null @@ -1,63 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; -import "package:flutter_product_page/src/services/selected_shop_service.dart"; -import "package:flutter_product_page/src/ui/widgets/horizontal_list_items.dart"; -import "package:flutter_product_page/src/ui/widgets/spaced_wrap.dart"; - -/// Shop selector widget that displays a list to navigate between shops. -class ShopSelector extends StatelessWidget { - /// Constructor for the shop selector. - const ShopSelector({ - required this.configuration, - required this.selectedShopService, - 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. - final SelectedShopService selectedShopService; - - /// List of shops. - final List shops; - - /// Callback when a shop is tapped. - final Function(ProductPageShop shop) onTap; - - /// Padding between the buttons. - final double paddingBetweenButtons; - - /// Padding on the buttons. - final double paddingOnButtons; - - @override - Widget build(BuildContext context) { - if (shops.length == 1) { - return const SizedBox.shrink(); - } - - if (configuration.shopSelectorStyle == ShopSelectorStyle.spacedWrap) { - return SpacedWrap( - shops: shops, - selectedItem: selectedShopService.selectedShop!.id, - onTap: onTap, - width: MediaQuery.of(context).size.width - (16 * 2), - paddingBetweenButtons: paddingBetweenButtons, - paddingOnButtons: paddingOnButtons, - ); - } - - return HorizontalListItems( - shops: shops, - selectedItem: selectedShopService.selectedShop!.id, - onTap: onTap, - paddingBetweenButtons: paddingBetweenButtons, - paddingOnButtons: paddingOnButtons, - ); - } -} diff --git a/packages/flutter_product_page/lib/src/ui/product_page.dart b/packages/flutter_product_page/lib/src/ui/product_page.dart deleted file mode 100644 index bbdfe8f..0000000 --- a/packages/flutter_product_page/lib/src/ui/product_page.dart +++ /dev/null @@ -1,252 +0,0 @@ -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/services/selected_shop_service.dart"; -import "package:flutter_product_page/src/services/shopping_cart_notifier.dart"; -import "package:flutter_product_page/src/ui/components/shop_selector.dart"; -import "package:flutter_product_page/src/ui/components/weekly_discount.dart"; - -/// A page that displays products. -class ProductPage extends StatelessWidget { - /// Constructor for the product page. - ProductPage({ - required this.configuration, - this.initialBuildShopId, - super.key, - }); - - /// Configuration for the product page. - final ProductPageConfiguration configuration; - - /// An optional initial shop ID to select. This overrides the initialShopId - /// from the configuration. - final String? initialBuildShopId; - - late final SelectedShopService _selectedShopService = SelectedShopService(); - - late final ShoppingCartNotifier _shoppingCartNotifier = - ShoppingCartNotifier(); - - @override - Widget build(BuildContext context) => Padding( - padding: configuration.pagePadding, - child: FutureBuilder( - future: configuration.shops, - builder: (BuildContext context, AsyncSnapshot data) { - if (data.connectionState == ConnectionState.waiting) { - return const Align( - alignment: Alignment.center, - child: CircularProgressIndicator.adaptive(), - ); - } - - if (data.hasError) { - return configuration.errorBuilder!( - context, - data.error, - data.stackTrace, - ); - } - - List? shops = data.data; - - if (shops == null || shops.isEmpty) { - return configuration.errorBuilder!(context, null, null); - } - - if (initialBuildShopId != null) { - ProductPageShop? initialShop; - - for (var shop in shops) { - if (shop.id == initialBuildShopId) { - initialShop = shop; - break; - } - } - - _selectedShopService.selectShop(initialShop ?? shops.first); - } else if (configuration.initialShopId != null) { - ProductPageShop? initialShop; - - for (var shop in shops) { - if (shop.id == configuration.initialShopId) { - initialShop = shop; - break; - } - } - - _selectedShopService.selectShop(initialShop ?? shops.first); - } else { - _selectedShopService.selectShop(shops.first); - } - - return ListenableBuilder( - listenable: _selectedShopService, - builder: (BuildContext context, Widget? _) { - configuration.onShopSelectionChange?.call( - _selectedShopService.selectedShop!, - ); - return _ProductPage( - configuration: configuration, - selectedShopService: _selectedShopService, - shoppingCartNotifier: _shoppingCartNotifier, - shops: shops, - ); - }, - ); - }, - ), - ); -} - -class _ProductPage extends StatelessWidget { - const _ProductPage({ - required this.configuration, - required this.selectedShopService, - required this.shoppingCartNotifier, - required this.shops, - }); - - final ProductPageConfiguration configuration; - final SelectedShopService selectedShopService; - final ShoppingCartNotifier shoppingCartNotifier; - - final List shops; - - void _onTapChangeShop(ProductPageShop shop) { - selectedShopService.selectShop(shop); - } - - @override - Widget build(BuildContext context) { - var pageContent = SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ShopSelector( - configuration: configuration, - selectedShopService: selectedShopService, - shops: shops, - onTap: _onTapChangeShop, - ), - _ShopContents( - configuration: configuration, - selectedShopService: selectedShopService, - shoppingCartNotifier: shoppingCartNotifier, - ), - ], - ), - ); - - return Stack( - children: [ - pageContent, - Align( - alignment: Alignment.bottomCenter, - child: configuration.navigateToShoppingCartBuilder( - context, - configuration, - shoppingCartNotifier, - ), - ), - ], - ); - } -} - -class _ShopContents extends StatelessWidget { - const _ShopContents({ - required this.configuration, - required this.selectedShopService, - required this.shoppingCartNotifier, - }); - - final ProductPageConfiguration configuration; - final SelectedShopService selectedShopService; - final ShoppingCartNotifier shoppingCartNotifier; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Padding( - padding: EdgeInsets.symmetric( - horizontal: configuration.pagePadding.horizontal, - ), - child: FutureBuilder( - // ignore: discarded_futures - future: configuration.getProducts( - selectedShopService.selectedShop!, - ), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const Align( - alignment: Alignment.center, - child: CircularProgressIndicator.adaptive(), - ); - } - - if (snapshot.hasError) { - return configuration.errorBuilder!( - context, - snapshot.error, - snapshot.stackTrace, - ); - } - - var productPageContent = snapshot.data; - - if (productPageContent == null || - productPageContent.products.isEmpty) { - return configuration.noContentBuilder!(context); - } - - var productList = Padding( - padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), - child: Column( - children: [ - // Products - getCategoryList( - context, - configuration, - shoppingCartNotifier, - productPageContent.products, - ), - - // Bottom padding so the last product is not cut off - // by the to shopping cart button. - const SizedBox(height: 48), - ], - ), - ); - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Discounted product - if (productPageContent.discountedProduct != null) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: WeeklyDiscount( - configuration: configuration, - product: productPageContent.discountedProduct!, - ), - ), - ], - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16, vertical: 24), - child: Text( - "What would you like to order?", - style: theme.textTheme.titleLarge, - textAlign: TextAlign.start, - ), - ), - - productList, - ], - ); - }, - ), - ); - } -} diff --git a/packages/flutter_product_page/lib/src/ui/product_page_screen.dart b/packages/flutter_product_page/lib/src/ui/product_page_screen.dart deleted file mode 100644 index ceaa158..0000000 --- a/packages/flutter_product_page/lib/src/ui/product_page_screen.dart +++ /dev/null @@ -1,36 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_product_page/src/configuration/product_page_configuration.dart"; -import "package:flutter_product_page/src/ui/product_page.dart"; - -/// A screen that displays a product page. This screen contains a Scaffold, -/// in which the body is a SafeArea that contains a ProductPage widget. -/// -/// If you do not wish to create a Scaffold you can use the -/// [ProductPage] widget directly. -class ProductPageScreen extends StatelessWidget { - /// Constructor for the product page screen. - const ProductPageScreen({ - required this.configuration, - this.initialBuildShopId, - super.key, - }); - - /// Configuration for the product page. - final ProductPageConfiguration configuration; - - /// An optional initial shop ID to select. This overrides the initialShopId - /// from the configuration. - final String? initialBuildShopId; - - @override - Widget build(BuildContext context) => Scaffold( - appBar: configuration.appBar!.call(context), - body: SafeArea( - child: ProductPage( - configuration: configuration, - initialBuildShopId: initialBuildShopId, - ), - ), - bottomNavigationBar: configuration.bottomNavigationBar, - ); -} 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 new file mode 100644 index 0000000..e81cc2d --- /dev/null +++ b/packages/flutter_product_page/lib/src/widgets/defaults/default_appbar.dart @@ -0,0 +1,45 @@ +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 new file mode 100644 index 0000000..8ddf940 --- /dev/null +++ b/packages/flutter_product_page/lib/src/widgets/defaults/default_error.dart @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000..00fa21f --- /dev/null +++ b/packages/flutter_product_page/lib/src/widgets/defaults/default_no_content.dart @@ -0,0 +1,18 @@ +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/ui/components/product_item.dart b/packages/flutter_product_page/lib/src/widgets/defaults/default_product_item.dart similarity index 87% rename from packages/flutter_product_page/lib/src/ui/components/product_item.dart rename to packages/flutter_product_page/lib/src/widgets/defaults/default_product_item.dart index ffcea2c..859592d 100644 --- a/packages/flutter_product_page/lib/src/ui/components/product_item.dart +++ b/packages/flutter_product_page/lib/src/widgets/defaults/default_product_item.dart @@ -1,16 +1,17 @@ import "package:cached_network_image/cached_network_image.dart"; import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.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 ProductItem extends StatelessWidget { +class DefaultProductItem extends StatelessWidget { /// Constructor for the product item widget. - const ProductItem({ + const DefaultProductItem({ required this.product, required this.onProductDetail, required this.onAddToCart, - required this.localizations, + required this.translations, super.key, }); @@ -18,13 +19,17 @@ class ProductItem extends StatelessWidget { final Product product; /// Function to call when the product detail is requested. - final Function(BuildContext context, Product selectedProduct) onProductDetail; + 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 ProductPageLocalization localizations; + final ProductPageTranslations translations; /// Size of the product image. static const double imageSize = 44; @@ -46,7 +51,7 @@ class ProductItem extends StatelessWidget { fit: BoxFit.cover, placeholder: (context, url) => loadingImageSkeleton, errorWidget: (context, url, error) => Tooltip( - message: localizations.failedToLoadImageExplenation, + message: translations.failedToLoadImageExplenation, child: Container( width: 48, height: 48, @@ -74,7 +79,11 @@ class ProductItem extends StatelessWidget { var productInformationIcon = Padding( padding: const EdgeInsets.only(left: 4), child: IconButton( - onPressed: () => onProductDetail(context, product), + onPressed: () => onProductDetail( + context, + product, + translations.close, + ), icon: Icon( Icons.info_outline, color: theme.colorScheme.primary, 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 new file mode 100644 index 0000000..1645ca9 --- /dev/null +++ b/packages/flutter_product_page/lib/src/widgets/defaults/default_shopping_cart_button.dart @@ -0,0 +1,71 @@ +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 new file mode 100644 index 0000000..2e5ee9a --- /dev/null +++ b/packages/flutter_product_page/lib/src/widgets/defaults/selected_categories.dart @@ -0,0 +1,72 @@ +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/ui/widgets/horizontal_list_items.dart b/packages/flutter_product_page/lib/src/widgets/horizontal_list_items.dart similarity index 93% rename from packages/flutter_product_page/lib/src/ui/widgets/horizontal_list_items.dart rename to packages/flutter_product_page/lib/src/widgets/horizontal_list_items.dart index 515f17b..bdd6495 100644 --- a/packages/flutter_product_page/lib/src/ui/widgets/horizontal_list_items.dart +++ b/packages/flutter_product_page/lib/src/widgets/horizontal_list_items.dart @@ -1,5 +1,5 @@ import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; /// Horizontal list of items. class HorizontalListItems extends StatelessWidget { @@ -14,7 +14,7 @@ class HorizontalListItems extends StatelessWidget { }); /// List of items. - final List shops; + final List shops; /// Selected item. final String selectedItem; @@ -26,7 +26,7 @@ class HorizontalListItems extends StatelessWidget { final double paddingOnButtons; /// Callback when an item is tapped. - final Function(ProductPageShop shop) onTap; + final Function(Shop shop) onTap; @override Widget build(BuildContext context) { diff --git a/packages/flutter_product_page/lib/src/ui/widgets/product_item_popup.dart b/packages/flutter_product_page/lib/src/widgets/product_item_popup.dart similarity index 90% rename from packages/flutter_product_page/lib/src/ui/widgets/product_item_popup.dart rename to packages/flutter_product_page/lib/src/widgets/product_item_popup.dart index 099fe49..77db847 100644 --- a/packages/flutter_product_page/lib/src/ui/widgets/product_item_popup.dart +++ b/packages/flutter_product_page/lib/src/widgets/product_item_popup.dart @@ -1,12 +1,12 @@ import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.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, + required this.closeText, super.key, }); @@ -14,7 +14,7 @@ class ProductItemPopup extends StatelessWidget { final Product product; /// Configuration for the product page. - final ProductPageConfiguration configuration; + final String closeText; @override Widget build(BuildContext context) { @@ -49,7 +49,7 @@ class ProductItemPopup extends StatelessWidget { vertical: 8.0, ), child: Text( - configuration.localizations.close, + 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 new file mode 100644 index 0000000..4ed4090 --- /dev/null +++ b/packages/flutter_product_page/lib/src/widgets/shop_selector.dart @@ -0,0 +1,85 @@ +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/ui/widgets/spaced_wrap.dart b/packages/flutter_product_page/lib/src/widgets/spaced_wrap.dart similarity index 93% rename from packages/flutter_product_page/lib/src/ui/widgets/spaced_wrap.dart rename to packages/flutter_product_page/lib/src/widgets/spaced_wrap.dart index 66cf0c8..24b19c5 100644 --- a/packages/flutter_product_page/lib/src/ui/widgets/spaced_wrap.dart +++ b/packages/flutter_product_page/lib/src/widgets/spaced_wrap.dart @@ -1,5 +1,5 @@ import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.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. @@ -16,7 +16,7 @@ class SpacedWrap extends StatelessWidget { }); /// List of items. - final List shops; + final List shops; /// Selected item. final String selectedItem; @@ -31,7 +31,7 @@ class SpacedWrap extends StatelessWidget { final double paddingOnButtons; /// Callback when an item is tapped. - final Function(ProductPageShop shop) onTap; + final Function(Shop shop) onTap; @override Widget build(BuildContext context) { diff --git a/packages/flutter_product_page/lib/src/ui/components/weekly_discount.dart b/packages/flutter_product_page/lib/src/widgets/weekly_discount.dart similarity index 90% rename from packages/flutter_product_page/lib/src/ui/components/weekly_discount.dart rename to packages/flutter_product_page/lib/src/widgets/weekly_discount.dart index e82a270..a22a02c 100644 --- a/packages/flutter_product_page/lib/src/ui/components/weekly_discount.dart +++ b/packages/flutter_product_page/lib/src/widgets/weekly_discount.dart @@ -1,6 +1,7 @@ import "package:cached_network_image/cached_network_image.dart"; import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.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 { @@ -27,7 +28,7 @@ class WeeklyDiscount extends StatelessWidget { var bottomText = Padding( padding: const EdgeInsets.all(20.0), child: Text( - configuration.getDiscountDescription!(product), + configuration.discountDescription!(product), style: theme.textTheme.bodyMedium, textAlign: TextAlign.left, ), @@ -50,7 +51,7 @@ class WeeklyDiscount extends StatelessWidget { Icons.error_outline_rounded, color: Colors.red, ), - Text(configuration.localizations.failedToLoadImageExplenation), + Text(configuration.translations.failedToLoadImageExplenation), ], ), ), @@ -86,7 +87,7 @@ class WeeklyDiscount extends StatelessWidget { horizontal: 16, ), child: Text( - configuration.localizations.discountTitle, + configuration.translations.discountTitle, style: theme.textTheme.headlineSmall, textAlign: TextAlign.left, ), diff --git a/packages/flutter_product_page/pubspec.yaml b/packages/flutter_product_page/pubspec.yaml index 2207ff0..c28c746 100644 --- a/packages/flutter_product_page/pubspec.yaml +++ b/packages/flutter_product_page/pubspec.yaml @@ -1,10 +1,10 @@ name: flutter_product_page description: "A Flutter module for the product page" -publish_to: 'none' +publish_to: "none" version: 2.0.0 environment: - sdk: '>=3.3.4 <4.0.0' + sdk: ">=3.3.4 <4.0.0" dependencies: flutter: @@ -15,11 +15,13 @@ dependencies: git: url: https://github.com/Iconica-Development/flutter_nested_categories ref: 0.0.1 - flutter_shopping: + flutter_shopping_interface: git: url: https://github.com/Iconica-Development/flutter_shopping - path: packages/flutter_shopping + path: packages/flutter_shopping_interface ref: 2.0.0 + collection: ^1.18.0 + provider: ^6.1.2 dev_dependencies: flutter_test: diff --git a/packages/flutter_shopping/example/.gitignore b/packages/flutter_shopping/example/.gitignore deleted file mode 100644 index 700e523..0000000 --- a/packages/flutter_shopping/example/.gitignore +++ /dev/null @@ -1,53 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# 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/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ -.metadata -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 - -# Platforms -android/ -ios/ -linux/ -macos/ -web/ -windows/ diff --git a/packages/flutter_shopping/example/README.md b/packages/flutter_shopping/example/README.md deleted file mode 100644 index 2b3fce4..0000000 --- a/packages/flutter_shopping/example/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# example - -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/packages/flutter_shopping/example/analysis_options.yaml b/packages/flutter_shopping/example/analysis_options.yaml deleted file mode 100644 index 2a97d5c..0000000 --- a/packages/flutter_shopping/example/analysis_options.yaml +++ /dev/null @@ -1,7 +0,0 @@ -include: package:flutter_iconica_analysis/analysis_options.yaml - -analyzer: - exclude: - -linter: - rules: diff --git a/packages/flutter_shopping/example/lib/main.dart b/packages/flutter_shopping/example/lib/main.dart deleted file mode 100644 index 1157c00..0000000 --- a/packages/flutter_shopping/example/lib/main.dart +++ /dev/null @@ -1,22 +0,0 @@ -import "package:example/src/routes.dart"; -import "package:example/src/utils/theme.dart"; -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; - -void main() { - runApp(const ProviderScope(child: MyApp())); -} - -class MyApp extends HookConsumerWidget { - const MyApp({ - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) => MaterialApp.router( - debugShowCheckedModeBanner: false, - restorationScopeId: "app", - theme: getTheme(), - routerConfig: ref.read(routerProvider), - ); -} diff --git a/packages/flutter_shopping/example/lib/src/configuration/configuration.dart b/packages/flutter_shopping/example/lib/src/configuration/configuration.dart deleted file mode 100644 index bb3454e..0000000 --- a/packages/flutter_shopping/example/lib/src/configuration/configuration.dart +++ /dev/null @@ -1,189 +0,0 @@ -import "package:example/src/routes.dart"; -import "package:example/src/services/order_service.dart"; -import "package:example/src/services/shop_service.dart"; -import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; -import "package:go_router/go_router.dart"; - -// (REQUIRED): Create your own instance of the ProductService. -final ProductService productService = ProductService([]); - -FlutterShoppingConfiguration getFlutterShoppingConfiguration() => - FlutterShoppingConfiguration( - // (REQUIRED): Shop builder configuration - shopBuilder: ( - BuildContext context, - String? initialBuildShopId, - String? streetName, - ) => - ProductPageScreen( - configuration: ProductPageConfiguration( - // (REQUIRED): List of shops that should be displayed - // If there is only one, make a list with just one shop. - shops: Future.value(getShops()), - - // (REQUIRED): Function to add a product to the cart - onAddToCart: productService.addProduct, - - // (REQUIRED): Function to get the products for a shop - getProducts: (ProductPageShop shop) => - Future.value( - getShopContent(shop.id), - ), - - // (REQUIRED): Function to navigate to the shopping cart - onNavigateToShoppingCart: () async => onCompleteProductPage(context), - - // (RECOMMENDED): Function to get the number of products in the - // shopping cart. This is used to display the number of products - // in the shopping cart on the product page. - getProductsInShoppingCart: productService.countProducts, - - // (RECOMMENDED) Function that returns the description for a - // product that is on sale. - getDiscountDescription: (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. - initialShopId: getShops().first.id, - - // (RECOMMENDED) Localizations for the product page. - localizations: const ProductPageLocalization(), - - // (OPTIONAL) Appbar - appBar: (context) => AppBar( - title: const Text("Shop"), - leading: IconButton( - icon: const Icon( - Icons.arrow_back, - color: Colors.white, - ), - onPressed: () { - context.go(homePage); - }, - ), - ), - ), - - // (OPTIONAL): Initial build shop id that overrides the initialShop - initialBuildShopId: initialBuildShopId, - ), - - // (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, service, config) => - ListTile( - title: Text(product.name), - subtitle: Text(product.price.toStringAsFixed(2)), - leading: Image.network( - product.imageUrl, - errorBuilder: (context, error, stackTrace) => const Tooltip( - message: "Error loading image", - child: Icon( - Icons.error, - color: Colors.red, - ), - ), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.remove), - onPressed: () => productService.removeOneProduct(product), - ), - Text("${product.quantity}"), - IconButton( - icon: const Icon(Icons.add), - onPressed: () => productService.addProduct(product), - ), - ], - ), - ), - - // (OPTIONAL/REQUIRED) on confirm order callback: - // Either use this callback or the placeOrderButtonBuilder. - onConfirmOrder: (products) async => onCompleteShoppingCart(context), - - // (RECOMMENDED) localizations: - localizations: const ShoppingCartLocalizations(), - - /// (OPTIONAL) no content builder for when there are no products - /// in the shopping cart. - noContentBuilder: (context) => const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 128), - child: Column( - children: [ - Icon( - Icons.warning, - ), - SizedBox( - height: 16, - ), - Text( - "Geen producten in winkelmandje", - ), - ], - ), - ), - ), - - // (OPTIONAL) custom appbar: - appBar: AppBar( - title: const Text("Shopping Cart"), - leading: IconButton( - icon: const Icon( - Icons.arrow_back, - color: Colors.white, - ), - onPressed: () { - context.go(FlutterShoppingPathRoutes.shop); - }, - ), - ), - ), - ), - - // (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 { - if (orderDetails.order["payment_option"] == "Pay now") { - // Make the user pay upfront. - } - - // If all went well, we can store the order in the database. - // Make sure to register whether or not the order was paid. - storeOrderInDatabase(productService.products, orderDetails); - - return true; - }, - ); diff --git a/packages/flutter_shopping/example/lib/src/models/my_shop.dart b/packages/flutter_shopping/example/lib/src/models/my_shop.dart deleted file mode 100644 index 9018fcc..0000000 --- a/packages/flutter_shopping/example/lib/src/models/my_shop.dart +++ /dev/null @@ -1,8 +0,0 @@ -import "package:flutter_shopping/flutter_shopping.dart"; - -class MyShop extends ProductPageShop { - const MyShop({ - required super.id, - required super.name, - }); -} diff --git a/packages/flutter_shopping/example/lib/src/routes.dart b/packages/flutter_shopping/example/lib/src/routes.dart deleted file mode 100644 index 4899345..0000000 --- a/packages/flutter_shopping/example/lib/src/routes.dart +++ /dev/null @@ -1,31 +0,0 @@ -import "package:example/src/configuration/configuration.dart"; -import "package:example/src/ui/homepage.dart"; -import "package:example/src/utils/go_router.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; -import "package:go_router/go_router.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; - -const String homePage = "/"; - -final routerProvider = Provider( - (ref) => GoRouter( - initialLocation: homePage, - routes: [ - // Flutter Shopping Story Routes - ...getShoppingStoryRoutes( - configuration: getFlutterShoppingConfiguration(), - ), - - // Home Route - GoRoute( - name: "home", - path: homePage, - pageBuilder: (context, state) => buildScreenWithFadeTransition( - context: context, - state: state, - child: const Homepage(), - ), - ), - ], - ), -); diff --git a/packages/flutter_shopping/example/lib/src/services/order_service.dart b/packages/flutter_shopping/example/lib/src/services/order_service.dart deleted file mode 100644 index 6078e6a..0000000 --- a/packages/flutter_shopping/example/lib/src/services/order_service.dart +++ /dev/null @@ -1,6 +0,0 @@ -import "package:flutter_shopping/flutter_shopping.dart"; - -/// Example implementation of storing an order in a database. -void storeOrderInDatabase(List products, OrderResult result) { - return; -} diff --git a/packages/flutter_shopping/example/lib/src/services/shop_service.dart b/packages/flutter_shopping/example/lib/src/services/shop_service.dart deleted file mode 100644 index a8e8f65..0000000 --- a/packages/flutter_shopping/example/lib/src/services/shop_service.dart +++ /dev/null @@ -1,49 +0,0 @@ -import "package:example/src/models/my_shop.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; - -/// This function should have your own implementation. Generally this would -/// contain some API call to fetch the list of shops. -List getShops() => [ - const MyShop(id: "1", name: "Shop 1"), - const MyShop(id: "2", name: "Shop 2"), - const MyShop(id: "3", name: "Shop 3"), - ]; - -ProductPageContent getShopContent(String shopId) { - var products = getProducts(shopId); - return ProductPageContent( - discountedProduct: products.first, - products: products, - ); -} - -/// This function should have your own implementation. Generally this would -/// contain some API call to fetch the list of products for a shop. -List getProducts(String shopId) => [ - Product( - id: "1", - name: "White bread", - price: 2.99, - category: "Loaves", - imageUrl: "https://via.placeholder.com/150", - hasDiscount: true, - discountPrice: 1.99, - description: "", - ), - Product( - id: "2", - name: "Brown bread", - price: 2.99, - category: "Loaves", - imageUrl: "https://via.placeholder.com/150", - description: "", - ), - Product( - id: "3", - name: "Cheese sandwich", - price: 1.99, - category: "Sandwiches", - imageUrl: "https://via.placeholder.com/150", - description: "", - ), - ]; diff --git a/packages/flutter_shopping/example/lib/src/ui/homepage.dart b/packages/flutter_shopping/example/lib/src/ui/homepage.dart deleted file mode 100644 index 46e1373..0000000 --- a/packages/flutter_shopping/example/lib/src/ui/homepage.dart +++ /dev/null @@ -1,20 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; -import "package:go_router/go_router.dart"; - -class Homepage extends StatelessWidget { - const Homepage({super.key}); - - @override - Widget build(BuildContext context) => Scaffold( - body: Center( - child: Badge( - label: const Text("1"), - child: IconButton( - icon: const Icon(Icons.shopping_cart_outlined, size: 50), - onPressed: () => context.go(FlutterShoppingPathRoutes.shop), - ), - ), - ), - ); -} diff --git a/packages/flutter_shopping/example/lib/src/utils/go_router.dart b/packages/flutter_shopping/example/lib/src/utils/go_router.dart deleted file mode 100644 index d7b239a..0000000 --- a/packages/flutter_shopping/example/lib/src/utils/go_router.dart +++ /dev/null @@ -1,26 +0,0 @@ -import "package:flutter/material.dart"; -import "package:go_router/go_router.dart"; - -CustomTransitionPage buildScreenWithFadeTransition({ - required BuildContext context, - required GoRouterState state, - required Widget child, -}) => - CustomTransitionPage( - key: state.pageKey, - child: child, - transitionsBuilder: (context, animation, secondaryAnimation, child) => - FadeTransition(opacity: animation, child: child), - ); - -CustomTransitionPage buildScreenWithoutTransition({ - required BuildContext context, - required GoRouterState state, - required Widget child, -}) => - CustomTransitionPage( - key: state.pageKey, - child: child, - transitionsBuilder: (context, animation, secondaryAnimation, child) => - child, - ); diff --git a/packages/flutter_shopping/example/lib/src/utils/theme.dart b/packages/flutter_shopping/example/lib/src/utils/theme.dart deleted file mode 100644 index a0bf1d8..0000000 --- a/packages/flutter_shopping/example/lib/src/utils/theme.dart +++ /dev/null @@ -1,32 +0,0 @@ -import "package:flutter/material.dart"; - -ThemeData getTheme() => ThemeData( - scaffoldBackgroundColor: const Color.fromRGBO(250, 249, 246, 1), - textTheme: const TextTheme( - labelMedium: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Colors.black, - ), - titleMedium: TextStyle( - fontSize: 16, - color: Color.fromRGBO(60, 60, 59, 1), - fontWeight: FontWeight.w700, - ), - ), - inputDecorationTheme: const InputDecorationTheme( - fillColor: Colors.white, - ), - colorScheme: const ColorScheme.light( - primary: Color.fromRGBO(64, 87, 122, 1), - secondary: Colors.white, - surface: Color.fromRGBO(250, 249, 246, 1), - ), - appBarTheme: const AppBarTheme( - backgroundColor: Color.fromRGBO(64, 87, 122, 1), - titleTextStyle: TextStyle( - fontSize: 28, - color: Colors.white, - ), - ), - ); diff --git a/packages/flutter_shopping/example/pubspec.yaml b/packages/flutter_shopping/example/pubspec.yaml deleted file mode 100644 index d9bf03b..0000000 --- a/packages/flutter_shopping/example/pubspec.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: example -description: Demonstrates how to use the flutter_shopping package." -publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.0 - -environment: - sdk: '>=3.3.4 <4.0.0' - -dependencies: - flutter: - sdk: flutter - flutter_hooks: ^0.20.0 - hooks_riverpod: ^2.1.1 - go_router: 12.1.3 - flutter_shopping: - git: - url: https://github.com/Iconica-Development/flutter_shopping - path: packages/flutter_shopping - 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: - uses-material-design: true - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic diff --git a/packages/flutter_shopping/example_amazon/.gitignore b/packages/flutter_shopping/example_amazon/.gitignore deleted file mode 100644 index 8760a85..0000000 --- a/packages/flutter_shopping/example_amazon/.gitignore +++ /dev/null @@ -1,53 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# 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/ - -# Flutter/Dart/Pub related -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.pub-cache/ -.pub/ -/build/ -.metadata -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 - -# Platforms -/android/ -/ios/ -/linux/ -/macos/ -/web/ -/windows/ diff --git a/packages/flutter_shopping/example_amazon/README.md b/packages/flutter_shopping/example_amazon/README.md deleted file mode 100644 index eddfc9c..0000000 --- a/packages/flutter_shopping/example_amazon/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# amazon - -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/packages/flutter_shopping/example_amazon/analysis_options.yaml b/packages/flutter_shopping/example_amazon/analysis_options.yaml deleted file mode 100644 index 0d29021..0000000 --- a/packages/flutter_shopping/example_amazon/analysis_options.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# 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_amazon/lib/main.dart b/packages/flutter_shopping/example_amazon/lib/main.dart deleted file mode 100644 index a48842e..0000000 --- a/packages/flutter_shopping/example_amazon/lib/main.dart +++ /dev/null @@ -1,22 +0,0 @@ -import "package:amazon/src/routes.dart"; -import "package:amazon/src/utils/theme.dart"; -import "package:flutter/material.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; - -void main() { - runApp(const ProviderScope(child: MyApp())); -} - -class MyApp extends HookConsumerWidget { - const MyApp({ - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) => MaterialApp.router( - debugShowCheckedModeBanner: false, - restorationScopeId: "app", - theme: getTheme(), - routerConfig: ref.read(routerProvider), - ); -} diff --git a/packages/flutter_shopping/example_amazon/lib/src/configuration/shopping_configuration.dart b/packages/flutter_shopping/example_amazon/lib/src/configuration/shopping_configuration.dart deleted file mode 100644 index 3bcce30..0000000 --- a/packages/flutter_shopping/example_amazon/lib/src/configuration/shopping_configuration.dart +++ /dev/null @@ -1,362 +0,0 @@ -import "package:amazon/src/routes.dart"; -import "package:amazon/src/services/category_service.dart"; -import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; -import "package:go_router/go_router.dart"; - -// (REQUIRED): Create your own instance of the ProductService. -final ProductService productService = ProductService([]); - -FlutterShoppingConfiguration getFlutterShoppingConfiguration() => - FlutterShoppingConfiguration( - // (REQUIRED): Shop builder configuration - shopBuilder: ( - BuildContext context, - String? initialBuildShopId, - String? streetName, - ) { - var theme = Theme.of(context); - - return ProductPageScreen( - configuration: ProductPageConfiguration( - // (REQUIRED): List of shops that should be displayed - // If there is only one, make a list with just one shop. - shops: Future.value(getCategories()), - - pagePadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), - - // (REQUIRED): Function to add a product to the cart - onAddToCart: (product) { - return productService.addProduct(product); - }, - - // (REQUIRED): Function to get the products for a shop - getProducts: (ProductPageShop shop) => - Future.value( - getShopContent(shop.id), - ), - - // (REQUIRED): Function to navigate to the shopping cart - onNavigateToShoppingCart: () => onCompleteProductPage(context), - - shopSelectorStyle: ShopSelectorStyle.row, - - navigateToShoppingCartBuilder: (context, productpageinfo, shop) { - return const SizedBox.shrink(); - }, - - bottomNavigationBar: BottomNavigationBar( - fixedColor: theme.primaryColor, - unselectedItemColor: Colors.black, - type: BottomNavigationBarType.fixed, - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.home), - label: "Home", - ), - BottomNavigationBarItem( - icon: Icon(Icons.person_2_outlined), - label: "Profile", - ), - BottomNavigationBarItem( - icon: Icon(Icons.shopping_cart_outlined), - label: "Cart", - ), - BottomNavigationBarItem( - icon: Icon(Icons.menu), - label: "Menu", - ), - ], - showSelectedLabels: false, - showUnselectedLabels: false, - onTap: (index) { - switch (index) { - case 0: - // context.go(homePage); - break; - case 1: - break; - case 2: - context.go(FlutterShoppingPathRoutes.shoppingCart); - break; - case 3: - break; - } - }, - ), - - productBuilder: (context, product) => Card( - elevation: 0, - color: const Color.fromARGB(255, 233, 233, 233), - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.zero, - ), - child: Row( - children: [ - Expanded( - flex: 3, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Image.network( - product.imageUrl, - loadingBuilder: (context, child, loadingProgress) => - loadingProgress == null - ? child - : const Center( - child: CircularProgressIndicator(), - ), - errorBuilder: (context, error, stackTrace) => - const Tooltip( - message: "Error loading image", - child: Icon( - Icons.error, - color: Colors.red, - ), - ), - ), - ), - ), - Expanded( - flex: 5, - child: ColoredBox( - color: theme.scaffoldBackgroundColor, - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - product.name, - style: theme.textTheme.titleMedium, - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - Row( - children: [ - Text( - "4.5", - style: theme.textTheme.bodyMedium?.copyWith( - color: Colors.blue, - ), - ), - const Icon(Icons.star, color: Colors.orange), - const Icon(Icons.star, color: Colors.orange), - const Icon(Icons.star, color: Colors.orange), - const Icon(Icons.star, color: Colors.orange), - const Icon(Icons.star_half, - color: Colors.orange), - Text( - "(3)", - style: theme.textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - ), - ], - ), - Text( - "\$${product.price.toStringAsFixed(2)}", - style: theme.textTheme.titleMedium, - ), - Text( - "Gratis bezorging door Amazon", - style: theme.textTheme.bodyMedium?.copyWith( - color: Colors.grey, - ), - ), - const SizedBox(height: 12), - FilledButton( - onPressed: () { - productService.addProduct(product); - }, - child: const Text("In winkelwagen"), - ), - ], - ), - ), - ), - ), - ], - ), - ), - - // (RECOMMENDED) The shop that is initially selected. - // Must be one of the shops in the [shops] list. - initialShopId: getCategories().first.id, - - // (RECOMMENDED) Localizations for the product page. - localizations: const ProductPageLocalization(), - - noContentBuilder: (context) => Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 128), - child: Column( - children: [ - const Icon( - Icons.warning, - size: 48, - ), - const SizedBox( - height: 16, - ), - Text( - "Geen producten gevonden", - style: theme.textTheme.titleLarge, - ), - ], - ), - ), - ), - - // (OPTIONAL) Appbar - appBar: (context) => AppBar( - title: const SizedBox( - height: 40, - child: SearchBar( - hintText: "Search products", - leading: Icon( - Icons.search, - color: Colors.black, - ), - trailing: [ - Icon( - Icons.fit_screen_outlined, - ), - ], - ), - ), - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - context.go(homePage); - }, - ), - bottom: AppBar( - backgroundColor: const Color.fromRGBO(203, 237, 230, 1), - title: Row( - children: [ - const Icon(Icons.location_on_outlined), - const SizedBox(width: 12), - Expanded( - child: Text( - "Bestemming: ${streetName ?? "Mark - 1234AB Doetinchem Nederland"}", - overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleMedium?.copyWith( - color: Colors.black, - ), - ), - ), - ], - ), - primary: false, - ), - ), - ), - - // (OPTIONAL): Initial build shop id that overrides the initialShop - initialBuildShopId: initialBuildShopId, - ); - }, - - // (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, service, config) => - ListTile( - title: Text(product.name), - subtitle: Text(product.price.toStringAsFixed(2)), - leading: Image.network( - product.imageUrl, - errorBuilder: (context, error, stackTrace) => const Tooltip( - message: "Error loading image", - child: Icon( - Icons.error, - color: Colors.red, - ), - ), - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: const Icon(Icons.remove), - onPressed: () => productService.removeOneProduct(product), - ), - Text("${product.quantity}"), - IconButton( - icon: const Icon(Icons.add), - onPressed: () => productService.addProduct(product), - ), - ], - ), - ), - - // (OPTIONAL/REQUIRED) on confirm order callback: - // Either use this callback or the placeOrderButtonBuilder. - onConfirmOrder: (products) => onCompleteShoppingCart(context), - - // (RECOMMENDED) localizations: - localizations: const ShoppingCartLocalizations(), - - /// (OPTIONAL) no content builder for when there are no products - /// in the shopping cart. - noContentBuilder: (context) => const Center( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 128), - child: Column( - children: [ - Icon( - Icons.warning, - ), - SizedBox( - height: 16, - ), - Text( - "Geen producten in winkelmandje", - ), - ], - ), - ), - ), - - // (OPTIONAL) custom appbar: - appBar: AppBar( - title: const Text("Shopping Cart"), - leading: IconButton( - icon: const Icon( - Icons.arrow_back, - color: Colors.white, - ), - onPressed: () { - context.go(FlutterShoppingPathRoutes.shop); - }, - ), - ), - ), - ), - - // (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 { - // return true; - // }, - ); diff --git a/packages/flutter_shopping/example_amazon/lib/src/models/my_category.dart b/packages/flutter_shopping/example_amazon/lib/src/models/my_category.dart deleted file mode 100644 index 0899d37..0000000 --- a/packages/flutter_shopping/example_amazon/lib/src/models/my_category.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:flutter_shopping/flutter_shopping.dart'; - -class MyCategory extends ProductPageShop { - const MyCategory({ - required super.id, - required super.name, - }); -} diff --git a/packages/flutter_shopping/example_amazon/lib/src/routes.dart b/packages/flutter_shopping/example_amazon/lib/src/routes.dart deleted file mode 100644 index 8e82277..0000000 --- a/packages/flutter_shopping/example_amazon/lib/src/routes.dart +++ /dev/null @@ -1,30 +0,0 @@ -import "package:amazon/src/configuration/shopping_configuration.dart"; -import "package:amazon/src/ui/homepage.dart"; -import "package:amazon/src/utils/go_router.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; -import "package:go_router/go_router.dart"; -import "package:hooks_riverpod/hooks_riverpod.dart"; - -const String homePage = "/"; - -final routerProvider = Provider( - (ref) => GoRouter( - initialLocation: homePage, - routes: [ - // Flutter Shopping Story Routes - ...getShoppingStoryRoutes( - configuration: getFlutterShoppingConfiguration(), - ), - // Home Route - GoRoute( - name: "home", - path: homePage, - pageBuilder: (context, state) => buildScreenWithFadeTransition( - context: context, - state: state, - child: const Homepage(), - ), - ), - ], - ), -); diff --git a/packages/flutter_shopping/example_amazon/lib/src/services/category_service.dart b/packages/flutter_shopping/example_amazon/lib/src/services/category_service.dart deleted file mode 100644 index 69af668..0000000 --- a/packages/flutter_shopping/example_amazon/lib/src/services/category_service.dart +++ /dev/null @@ -1,93 +0,0 @@ -import "package:amazon/src/models/my_category.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; - -Map categories = { - "Electronics": "Electronica", - "Smart phones": "Telefoons", - "TV's": "TV's", -}; - -List allProducts() => [ - Product( - id: "1", - name: - "Skar Audio Single 8\" Complete 1,200 Watt EVL Series Subwoofer Bass Package - Includes Loaded Enclosure with...", - price: 2.99, - category: categories["Electronics"]!, - imageUrl: - "https://m.media-amazon.com/images/I/710n3hnbfXL._AC_UY218_.jpg", - description: "", - ), - Product( - id: "2", - name: - "Frameo 10.1 Inch WiFi Digital Picture Frame, 1280x800 HD IPS Touch Screen Photo Frame Electronic, 32GB Memory, Auto...", - price: 2.99, - category: categories["Electronics"]!, - imageUrl: - "https://m.media-amazon.com/images/I/61O+aorCp0L._AC_UY218_.jpg", - description: "", - ), - Product( - id: "3", - name: - "STREBITO Electronics Precision Screwdriver Sets 142-Piece with 120 Bits Magnetic Repair Tool Kit for iPhone, MacBook,...", - price: 1.99, - category: categories["Electronics"]!, - imageUrl: - "https://m.media-amazon.com/images/I/81-C7lGtQsL._AC_UY218_.jpg", - description: "", - ), - Product( - id: "4", - name: - "Samsung Galaxy A15 (SM-155M/DSN), 128GB 6GB RAM, Dual SIM, Factory Unlocked GSM, International Version (Wall...", - price: 1.99, - category: categories["Smart phones"]!, - imageUrl: - "https://m.media-amazon.com/images/I/51rp0nqaPoL._AC_UY218_.jpg", - description: "", - ), - Product( - id: "5", - name: - "SAMSUNG Galaxy S24 Ultra Cell Phone, 512GB AI Smartphone, Unlocked Android, 50MP Zoom Camera, Long...", - price: 1.99, - category: categories["Smart phones"]!, - imageUrl: - "https://m.media-amazon.com/images/I/71ZoDT7a2wL._AC_UY218_.jpg", - description: "", - ), - ]; - -List getCategories() => [ - MyCategory(id: "1", name: categories["Electronics"]!), - MyCategory(id: "2", name: categories["Smart phones"]!), - MyCategory(id: "3", name: categories["TV's"]!), - const MyCategory(id: "4", name: "Monitoren"), - const MyCategory(id: "5", name: "Speakers"), - const MyCategory(id: "6", name: "Toetsenborden"), - ]; - -ProductPageContent getShopContent(String shopId) { - var products = getProducts(shopId); - return ProductPageContent( - products: products, - ); -} - -List getProducts(String categoryId) { - if (categoryId == "1") { - return allProducts(); - } else if (categoryId == "2") { - return allProducts() - .where((product) => product.category == categories["Smart phones"]!) - .toList(); - } else if (categoryId == "3") { - return allProducts() - .where((product) => product.category == categories["TV's"]!) - .toList(); - } else { - return []; - } -} diff --git a/packages/flutter_shopping/example_amazon/lib/src/ui/homepage.dart b/packages/flutter_shopping/example_amazon/lib/src/ui/homepage.dart deleted file mode 100644 index 46e1373..0000000 --- a/packages/flutter_shopping/example_amazon/lib/src/ui/homepage.dart +++ /dev/null @@ -1,20 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; -import "package:go_router/go_router.dart"; - -class Homepage extends StatelessWidget { - const Homepage({super.key}); - - @override - Widget build(BuildContext context) => Scaffold( - body: Center( - child: Badge( - label: const Text("1"), - child: IconButton( - icon: const Icon(Icons.shopping_cart_outlined, size: 50), - onPressed: () => context.go(FlutterShoppingPathRoutes.shop), - ), - ), - ), - ); -} diff --git a/packages/flutter_shopping/example_amazon/lib/src/utils/go_router.dart b/packages/flutter_shopping/example_amazon/lib/src/utils/go_router.dart deleted file mode 100644 index d7b239a..0000000 --- a/packages/flutter_shopping/example_amazon/lib/src/utils/go_router.dart +++ /dev/null @@ -1,26 +0,0 @@ -import "package:flutter/material.dart"; -import "package:go_router/go_router.dart"; - -CustomTransitionPage buildScreenWithFadeTransition({ - required BuildContext context, - required GoRouterState state, - required Widget child, -}) => - CustomTransitionPage( - key: state.pageKey, - child: child, - transitionsBuilder: (context, animation, secondaryAnimation, child) => - FadeTransition(opacity: animation, child: child), - ); - -CustomTransitionPage buildScreenWithoutTransition({ - required BuildContext context, - required GoRouterState state, - required Widget child, -}) => - CustomTransitionPage( - key: state.pageKey, - child: child, - transitionsBuilder: (context, animation, secondaryAnimation, child) => - child, - ); diff --git a/packages/flutter_shopping/example_amazon/lib/src/utils/theme.dart b/packages/flutter_shopping/example_amazon/lib/src/utils/theme.dart deleted file mode 100644 index 0342db9..0000000 --- a/packages/flutter_shopping/example_amazon/lib/src/utils/theme.dart +++ /dev/null @@ -1,43 +0,0 @@ -import "package:flutter/material.dart"; - -ThemeData getTheme() => ThemeData( - scaffoldBackgroundColor: const Color.fromRGBO(250, 249, 246, 1), - textTheme: const TextTheme( - labelMedium: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Colors.black, - ), - titleMedium: TextStyle( - fontSize: 16, - color: Color.fromRGBO(60, 60, 59, 1), - fontWeight: FontWeight.w700, - ), - ), - inputDecorationTheme: const InputDecorationTheme( - fillColor: Colors.white, - ), - colorScheme: const ColorScheme.light( - primary: Color.fromRGBO(161, 203, 211, 1), - secondary: Color.fromRGBO(221, 235, 238, 1), - surface: Color.fromRGBO(255, 255, 255, 1), - ), - appBarTheme: const AppBarTheme( - backgroundColor: Color.fromRGBO(161, 220, 218, 1), - foregroundColor: Colors.black, - titleTextStyle: TextStyle( - fontSize: 28, - color: Colors.white, - ), - ), - filledButtonTheme: FilledButtonThemeData( - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - Colors.yellow, - ), - foregroundColor: WidgetStateProperty.all( - Colors.black, - ), - ), - ), - ); diff --git a/packages/flutter_shopping/example_amazon/pubspec.yaml b/packages/flutter_shopping/example_amazon/pubspec.yaml deleted file mode 100644 index fb6267b..0000000 --- a/packages/flutter_shopping/example_amazon/pubspec.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: amazon -description: "A new Flutter project." -publish_to: 'none' -version: 1.0.0 - -environment: - sdk: '>=3.4.1 <4.0.0' - -dependencies: - flutter: - sdk: flutter - flutter_hooks: ^0.20.0 - hooks_riverpod: ^2.1.1 - go_router: 12.1.3 - flutter_nested_categories: - git: - url: https://github.com/Iconica-Development/flutter_nested_categories - ref: 0.0.1 - flutter_shopping: - git: - url: https://github.com/Iconica-Development/flutter_shopping - path: packages/flutter_shopping - ref: 2.0.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^3.0.0 - -flutter: - uses-material-design: true diff --git a/packages/flutter_shopping/example_amazon/test/widget_test.dart b/packages/flutter_shopping/example_amazon/test/widget_test.dart deleted file mode 100644 index 2c6d019..0000000 --- a/packages/flutter_shopping/example_amazon/test/widget_test.dart +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Iconica -// -// SPDX-License-Identifier: BSD-3-Clause - -import "package:flutter_test/flutter_test.dart"; - -void main() { - test("", () { - expect(true, true); - }); -} diff --git a/packages/flutter_shopping/lib/flutter_shopping.dart b/packages/flutter_shopping/lib/flutter_shopping.dart index 5e962e1..7732379 100644 --- a/packages/flutter_shopping/lib/flutter_shopping.dart +++ b/packages/flutter_shopping/lib/flutter_shopping.dart @@ -5,8 +5,5 @@ 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"; -export "src/config/flutter_shopping_configuration.dart"; -export "src/models/product.dart"; -export "src/routes.dart"; -export "src/user_stores/flutter_shopping_userstory_go_router.dart"; -export "src/user_stores/flutter_shopping_userstory_navigation.dart"; +export "src/configuration/shopping_configuration.dart"; +export "src/flutter_shopping_navigator_userstory.dart"; diff --git a/packages/flutter_shopping/lib/src/config/flutter_shopping_configuration.dart b/packages/flutter_shopping/lib/src/config/flutter_shopping_configuration.dart deleted file mode 100644 index 7b18e74..0000000 --- a/packages/flutter_shopping/lib/src/config/flutter_shopping_configuration.dart +++ /dev/null @@ -1,43 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; - -/// Configuration class for the flutter_shopping user-story. -class FlutterShoppingConfiguration { - /// Constructor for the FlutterShoppingConfiguration. - const FlutterShoppingConfiguration({ - required this.shopBuilder, - required this.shoppingCartBuilder, - required this.onCompleteUserStory, - this.orderDetailsBuilder, - this.onCompleteOrderDetails, - this.orderSuccessBuilder, - this.orderFailedBuilder, - }); - - /// Builder for the shop/product page. - final Widget Function( - BuildContext context, - String? initialBuildShopId, - String? streetName, - ) shopBuilder; - - /// Builder for the shopping cart page. - final Widget Function(BuildContext context) shoppingCartBuilder; - - /// Function that is called when the user-story is completed. - final Function(BuildContext context) onCompleteUserStory; - - /// Builder for the order details page. This does not have to be set if you - /// are using the default order details page. - final Widget Function(BuildContext context)? orderDetailsBuilder; - - /// Allows you to execute actions before - final Future Function(BuildContext context, OrderResult result)? - onCompleteOrderDetails; - - /// Builder for when the order is successful. - final Widget Function(BuildContext context)? orderSuccessBuilder; - - /// Builder for when the order failed. - final Widget Function(BuildContext context)? orderFailedBuilder; -} diff --git a/packages/flutter_shopping/lib/src/config/order_details_localizations.dart b/packages/flutter_shopping/lib/src/config/order_details_localizations.dart deleted file mode 100644 index af50182..0000000 --- a/packages/flutter_shopping/lib/src/config/order_details_localizations.dart +++ /dev/null @@ -1,10 +0,0 @@ -/// Contains the localized strings for the order details screen. -class OrderDetailsLocalizations { - /// Creates order details localizations - OrderDetailsLocalizations({ - this.orderDetailsTitle = "Information", - }); - - /// Title for the order details screen. - final String orderDetailsTitle; -} diff --git a/packages/flutter_shopping/lib/src/configuration/shopping_configuration.dart b/packages/flutter_shopping/lib/src/configuration/shopping_configuration.dart new file mode 100644 index 0000000..2cb20bb --- /dev/null +++ b/packages/flutter_shopping/lib/src/configuration/shopping_configuration.dart @@ -0,0 +1,240 @@ +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 new file mode 100644 index 0000000..b19bbb7 --- /dev/null +++ b/packages/flutter_shopping/lib/src/flutter_shopping_navigator_userstory.dart @@ -0,0 +1,235 @@ +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, + super.key, + }); + + /// Shopping configuration. + final ShoppingConfiguration? shoppingConfiguration; + + @override + Widget build(BuildContext context) => ShoppingProductPage( + shoppingConfiguration: shoppingConfiguration ?? + ShoppingConfiguration( + shoppingService: LocalShoppingService(), + ), + ); +} + +/// 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; + + @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(); + } + }, + getProducts: (shop) async { + if (shoppingConfiguration.onGetProducts != null) { + return shoppingConfiguration.onGetProducts!(shop.id); + } else { + return service.productService.getProducts(shop.id); + } + }, + onAddToCart: (product) { + if (shoppingConfiguration.onAddToCart != null) { + shoppingConfiguration.onAddToCart!(product); + return; + } else { + return service.shoppingCartService.addProduct(product); + } + }, + onNavigateToShoppingCart: () async { + if (shoppingConfiguration.onNavigateToShoppingCart != null) { + return shoppingConfiguration.onNavigateToShoppingCart!(); + } else { + return Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ShoppingCart( + shoppingConfiguration: shoppingConfiguration, + ), + ), + ); + } + }, + getProductsInShoppingCart: () { + if (shoppingConfiguration.getProductsInShoppingCart != null) { + return shoppingConfiguration.getProductsInShoppingCart!(); + } else { + return service.shoppingCartService.countProducts(); + } + }, + ), + ); + } +} + +/// 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/go_router.dart b/packages/flutter_shopping/lib/src/go_router.dart deleted file mode 100644 index cce7de5..0000000 --- a/packages/flutter_shopping/lib/src/go_router.dart +++ /dev/null @@ -1,28 +0,0 @@ -import "package:flutter/material.dart"; -import "package:go_router/go_router.dart"; - -/// Builder with a fade transition for when navigating to a new screen. -CustomTransitionPage buildScreenWithFadeTransition({ - required BuildContext context, - required GoRouterState state, - required Widget child, -}) => - CustomTransitionPage( - key: state.pageKey, - child: child, - transitionsBuilder: (context, animation, secondaryAnimation, child) => - FadeTransition(opacity: animation, child: child), - ); - -/// Builder without a transition for when navigating to a new screen. -CustomTransitionPage buildScreenWithoutTransition({ - required BuildContext context, - required GoRouterState state, - required Widget child, -}) => - CustomTransitionPage( - key: state.pageKey, - child: child, - transitionsBuilder: (context, animation, secondaryAnimation, child) => - child, - ); diff --git a/packages/flutter_shopping/lib/src/models/product.dart b/packages/flutter_shopping/lib/src/models/product.dart deleted file mode 100644 index 680b5f5..0000000 --- a/packages/flutter_shopping/lib/src/models/product.dart +++ /dev/null @@ -1,43 +0,0 @@ -/// The product class contains all the information that a product can have. -/// This class is used in the shopping cart and the product page. -class Product { - /// Creates a product. - 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, - }); - - /// The unique identifier for the product. - final String id; - - /// The name of the product. - final String name; - - /// The image URL of the product. - final String imageUrl; - - /// The category of the product. - final String category; - - /// The price of the product. - final double price; - - /// Whether the product has a discount or not. - final bool hasDiscount; - - /// The discounted price of the product. Only used if [hasDiscount] is true. - final double? discountPrice; - - /// Quantity for the product. - int quantity; - - /// The description of the product. - final String description; -} diff --git a/packages/flutter_shopping/lib/src/routes.dart b/packages/flutter_shopping/lib/src/routes.dart deleted file mode 100644 index e3730ca..0000000 --- a/packages/flutter_shopping/lib/src/routes.dart +++ /dev/null @@ -1,35 +0,0 @@ -/// All the name routes used in the user-story. -mixin FlutterShoppingNameRoutes { - /// The shop name route. - static const String shop = "shop"; - - /// The shopping cart name route. - static const String shoppingCart = "shoppingcart"; - - /// The order details name route. - static const String orderDetails = "orderdetails"; - - /// The order success name route. - static const String orderSuccess = "ordersuccess"; - - /// The order failed name route. - static const String orderFailed = "orderfailed"; -} - -/// All the path routes used in the user-story. -mixin FlutterShoppingPathRoutes { - /// The shop page route. - static const String shop = "/shop"; - - /// The shopping cart page route. - static const String shoppingCart = "/shopping-cart"; - - /// The order details page route. - static const String orderDetails = "/order-details"; - - /// The order success page route. - static const String orderSuccess = "/order-success"; - - /// The order failed page route. - static const String orderFailed = "/order-failed"; -} diff --git a/packages/flutter_shopping/lib/src/user_stores/flutter_shopping_userstory_go_router.dart b/packages/flutter_shopping/lib/src/user_stores/flutter_shopping_userstory_go_router.dart deleted file mode 100644 index 1e8b165..0000000 --- a/packages/flutter_shopping/lib/src/user_stores/flutter_shopping_userstory_go_router.dart +++ /dev/null @@ -1,52 +0,0 @@ -import "package:flutter_shopping/flutter_shopping.dart"; -import "package:flutter_shopping/src/widgets/default_order_failed_widget.dart"; -import "package:flutter_shopping/src/widgets/default_order_succes_widget.dart"; -import "package:go_router/go_router.dart"; - -/// All the routes for the shopping story. -List getShoppingStoryRoutes({ - required FlutterShoppingConfiguration configuration, -}) => - [ - GoRoute( - name: FlutterShoppingNameRoutes.shop, - path: FlutterShoppingPathRoutes.shop, - builder: (context, state) => configuration.shopBuilder( - context, - state.uri.queryParameters["id"], - state.uri.queryParameters["street"], - ), - ), - GoRoute( - name: FlutterShoppingNameRoutes.shoppingCart, - path: FlutterShoppingPathRoutes.shoppingCart, - builder: (context, state) => configuration.shoppingCartBuilder(context), - ), - GoRoute( - name: FlutterShoppingNameRoutes.orderDetails, - path: FlutterShoppingPathRoutes.orderDetails, - builder: (context, state) => configuration.orderDetailsBuilder != null - ? configuration.orderDetailsBuilder!(context) - : OrderDetailScreen( - configuration: OrderDetailConfiguration( - onCompleted: (result) { - context.go(FlutterShoppingPathRoutes.orderSuccess); - }, - ), - ), - ), - GoRoute( - name: FlutterShoppingNameRoutes.orderSuccess, - path: FlutterShoppingPathRoutes.orderSuccess, - builder: (context, state) => configuration.orderSuccessBuilder != null - ? configuration.orderSuccessBuilder!(context) - : DefaultOrderSucces(configuration: configuration), - ), - GoRoute( - name: FlutterShoppingNameRoutes.orderFailed, - path: FlutterShoppingPathRoutes.orderFailed, - builder: (context, state) => configuration.orderFailedBuilder != null - ? configuration.orderFailedBuilder!(context) - : DefaultOrderFailed(configuration: configuration), - ), - ]; diff --git a/packages/flutter_shopping/lib/src/user_stores/flutter_shopping_userstory_navigation.dart b/packages/flutter_shopping/lib/src/user_stores/flutter_shopping_userstory_navigation.dart deleted file mode 100644 index 0926a0f..0000000 --- a/packages/flutter_shopping/lib/src/user_stores/flutter_shopping_userstory_navigation.dart +++ /dev/null @@ -1,52 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; -import "package:go_router/go_router.dart"; - -/// Default on complete order details function. -/// This function will navigate to the order success or order failed page. -/// -/// You can create your own implementation if you decide to use a different -/// approach. -Future onCompleteOrderDetails( - BuildContext context, - FlutterShoppingConfiguration configuration, - OrderResult result, -) async { - var go = context.go; - var succesful = true; - - if (configuration.onCompleteOrderDetails != null) { - var executionResult = - await configuration.onCompleteOrderDetails?.call(context, result); - - if (executionResult == null || !executionResult) { - succesful = false; - } - } - - if (succesful) { - go(FlutterShoppingPathRoutes.orderSuccess); - } else { - go(FlutterShoppingPathRoutes.orderFailed); - } -} - -/// Default on complete shopping cart function. -/// -/// You can create your own implementation if you decide to use a different -/// approach. -Future onCompleteShoppingCart( - BuildContext context, -) async { - await context.push(FlutterShoppingPathRoutes.orderDetails); -} - -/// Default on complete product page function. -/// -/// You can create your own implementation if you decide to use a different -/// approach. -Future onCompleteProductPage( - BuildContext context, -) async { - await context.push(FlutterShoppingPathRoutes.shoppingCart); -} diff --git a/packages/flutter_shopping/lib/src/widgets/default_order_failed_widget.dart b/packages/flutter_shopping/lib/src/widgets/default_order_failed_widget.dart deleted file mode 100644 index 24debae..0000000 --- a/packages/flutter_shopping/lib/src/widgets/default_order_failed_widget.dart +++ /dev/null @@ -1,65 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; - -/// Default order failed widget. -class DefaultOrderFailed extends StatelessWidget { - /// Constructor for the DefaultOrderFailed. - const DefaultOrderFailed({ - required this.configuration, - super.key, - }); - - /// Configuration for the user-story. - final FlutterShoppingConfiguration configuration; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - var finishOrderButton = FilledButton( - onPressed: () => configuration.onCompleteUserStory(context), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 32.0, - vertical: 8.0, - ), - child: Text("Go back".toUpperCase()), - ), - ); - - var content = Column( - children: [ - const Spacer(), - const Icon( - Icons.error, - size: 100, - color: Colors.red, - ), - const SizedBox(height: 16), - Text( - "Uh oh.", - style: theme.textTheme.titleLarge, - ), - const SizedBox(height: 32), - Text( - "It seems that something went wrong.", - style: theme.textTheme.bodyMedium, - ), - Text( - "Please try again later.", - style: theme.textTheme.bodyMedium, - ), - const Spacer(), - finishOrderButton, - ], - ); - - return Scaffold( - body: SafeArea( - child: Center( - child: content, - ), - ), - ); - } -} diff --git a/packages/flutter_shopping/pubspec.yaml b/packages/flutter_shopping/pubspec.yaml index ffbd981..309d6c7 100644 --- a/packages/flutter_shopping/pubspec.yaml +++ b/packages/flutter_shopping/pubspec.yaml @@ -1,10 +1,10 @@ name: flutter_shopping description: "A new Flutter project." -publish_to: 'none' +publish_to: "none" version: 2.0.0 environment: - sdk: '>=3.3.4 <4.0.0' + sdk: ">=3.3.4 <4.0.0" dependencies: flutter: @@ -25,6 +25,16 @@ dependencies: 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 dev_dependencies: flutter_test: @@ -35,4 +45,3 @@ dev_dependencies: ref: 7.0.0 flutter: - diff --git a/packages/flutter_shopping_cart/lib/flutter_shopping_cart.dart b/packages/flutter_shopping_cart/lib/flutter_shopping_cart.dart index 4b22672..4599843 100644 --- a/packages/flutter_shopping_cart/lib/flutter_shopping_cart.dart +++ b/packages/flutter_shopping_cart/lib/flutter_shopping_cart.dart @@ -2,6 +2,5 @@ library flutter_shopping_cart; export "src/config/shopping_cart_config.dart"; -export "src/config/shopping_cart_localizations.dart"; -export "src/services/product_service.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 index d2a47a0..6a5ff3e 100644 --- a/packages/flutter_shopping_cart/lib/src/config/shopping_cart_config.dart +++ b/packages/flutter_shopping_cart/lib/src/config/shopping_cart_config.dart @@ -1,209 +1,89 @@ import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; -import "package:flutter_shopping_cart/src/widgets/product_item_popup.dart"; - -Widget _defaultNoContentBuilder(BuildContext context) => - const SizedBox.shrink(); +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 { +class ShoppingCartConfig { /// Creates a shopping cart configuration. ShoppingCartConfig({ - required this.productService, - this.productItemBuilder = _defaultProductItemBuilder, - this.onConfirmOrder, + required this.service, + required this.onConfirmOrder, + this.productItemBuilder, this.confirmOrderButtonBuilder, this.confirmOrderButtonHeight = 100, this.sumBottomSheetBuilder, this.sumBottomSheetHeight = 100, this.titleBuilder, - this.localizations = const ShoppingCartLocalizations(), - this.padding = const EdgeInsets.symmetric(horizontal: 32), + this.translations = const ShoppingCartTranslations(), + this.pagePadding = const EdgeInsets.symmetric(horizontal: 32), this.bottomPadding = const EdgeInsets.fromLTRB(44, 0, 44, 32), - this.appBar, - Widget Function(BuildContext context) noContentBuilder = - _defaultNoContentBuilder, - }) : assert( - confirmOrderButtonBuilder != null || onConfirmOrder != null, - """ -If you override the confirm order button builder, -you cannot use the onConfirmOrder callback.""", - ), - assert( - confirmOrderButtonBuilder == null || onConfirmOrder == null, - """ -If you do not override the confirm order button builder, -you must use the onConfirmOrder callback.""", - ), - _noContentBuilder = noContentBuilder; + this.appBarBuilder, + }); - /// Product Service. The service contains all the products that - /// a shopping cart can contain. Each product must extend the [Product] class. - /// The service is used to add, remove, and update products. - /// - /// The service can be seperate for each shopping cart in-case you want to - /// support seperate shopping carts for shop. - ProductService productService = ProductService([]); + /// 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. - final Widget Function( + Widget Function( BuildContext context, - Locale locale, Product product, - ProductService productService, ShoppingCartConfig configuration, - ) productItemBuilder; - - final Widget Function(BuildContext context) _noContentBuilder; - - /// No content builder. This builder is used to build the no content widget - /// that will be displayed in the shopping cart when there are no products. - Widget Function(BuildContext context) get noContentBuilder => - _noContentBuilder; + )? 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 - final Widget Function(BuildContext context)? confirmOrderButtonBuilder; + 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. - final double confirmOrderButtonHeight; + 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; + 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. - final Widget Function(BuildContext context)? sumBottomSheetBuilder; + 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. - final double sumBottomSheetHeight; + double sumBottomSheetHeight; /// Padding around the shopping cart. The padding is used to create space /// around the shopping cart. - final EdgeInsets padding; + 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. - final EdgeInsets bottomPadding; + EdgeInsets bottomPadding; - /// Title builder. This builder is used to build the title of the shopping - /// cart. The title is displayed at the top of the shopping cart. If you - /// use the title builder, the [title] will be ignored. - final Widget Function(BuildContext context)? titleBuilder; + /// Title builder. This builder is used to + /// build the title of the shopping cart. + final Widget Function( + BuildContext context, + String title, + )? titleBuilder; - /// Shopping cart localizations. The localizations are used to localize the - /// shopping cart. - final ShoppingCartLocalizations localizations; + /// Shopping cart translations. The translations for the shopping cart. + ShoppingCartTranslations translations; - /// App bar for the shopping cart screen. - final PreferredSizeWidget? appBar; -} - -Widget _defaultProductItemBuilder( - BuildContext context, - Locale locale, - Product product, - ProductService service, - ShoppingCartConfig configuration, -) { - 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( - 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: [ - 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: () => service.removeOneProduct(product), - ), - 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: () => service.addProduct(product), - ), - ], - ), - ], - ), - ), - ); + /// Appbar for the shopping cart screen. + PreferredSizeWidget Function(BuildContext context)? appBarBuilder; } diff --git a/packages/flutter_shopping_cart/lib/src/config/shopping_cart_localizations.dart b/packages/flutter_shopping_cart/lib/src/config/shopping_cart_translations.dart similarity index 52% rename from packages/flutter_shopping_cart/lib/src/config/shopping_cart_localizations.dart rename to packages/flutter_shopping_cart/lib/src/config/shopping_cart_translations.dart index b1c793d..a6deeb9 100644 --- a/packages/flutter_shopping_cart/lib/src/config/shopping_cart_localizations.dart +++ b/packages/flutter_shopping_cart/lib/src/config/shopping_cart_translations.dart @@ -1,24 +1,14 @@ -import "package:flutter/material.dart"; - /// Shopping cart localizations -class ShoppingCartLocalizations { +class ShoppingCartTranslations { /// Creates shopping cart localizations - const ShoppingCartLocalizations({ - this.locale = const Locale("en", "US"), + const ShoppingCartTranslations({ this.placeOrder = "Order", this.sum = "Subtotal:", this.cartTitle = "Products", this.close = "close", }); - /// Locale for the shopping cart. - /// This locale will be used to format the currency. - /// Default is English. - final Locale locale; - - /// Localization for the place order button. - /// This text will only be displayed if you're not using the place order - /// button builder. + /// Text for the place order button. final String placeOrder; /// Localization for the sum. diff --git a/packages/flutter_shopping_cart/lib/src/services/product_service.dart b/packages/flutter_shopping_cart/lib/src/services/product_service.dart deleted file mode 100644 index a96989d..0000000 --- a/packages/flutter_shopping_cart/lib/src/services/product_service.dart +++ /dev/null @@ -1,71 +0,0 @@ -import "package:flutter/foundation.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; - -/// Product service. This class is responsible for managing the products. -/// The service is used to add, remove, and update products. -class ProductService extends ChangeNotifier { - /// Creates a product service. - ProductService(this.products); - - /// List of products in the shopping cart. - final List products; - - /// Adds a product to the shopping cart. - void addProduct(T product) { - for (var p in products) { - if (p.id == product.id) { - p.quantity++; - notifyListeners(); - return; - } - } - - products.add(product); - notifyListeners(); - } - - /// Removes a product from the shopping cart. - void removeProduct(T product) { - for (var p in products) { - if (p.id == product.id) { - products.remove(p); - notifyListeners(); - return; - } - } - notifyListeners(); - } - - /// Removes one product from the shopping cart. - void removeOneProduct(T product) { - for (var p in products) { - if (p.id == product.id) { - if (p.quantity > 1) { - p.quantity--; - notifyListeners(); - return; - } - } - } - - products.remove(product); - notifyListeners(); - } - - /// Counts the number of products in the shopping cart. - int countProducts() { - var count = 0; - - for (var product in products) { - count += product.quantity; - } - - return count; - } - - /// Empties the shopping cart. - void clear() { - products.clear(); - notifyListeners(); - } -} diff --git a/packages/flutter_shopping_cart/lib/src/widgets/default_appbar.dart b/packages/flutter_shopping_cart/lib/src/widgets/default_appbar.dart new file mode 100644 index 0000000..b348975 --- /dev/null +++ b/packages/flutter_shopping_cart/lib/src/widgets/default_appbar.dart @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..d2a40fd --- /dev/null +++ b/packages/flutter_shopping_cart/lib/src/widgets/default_confirm_order_button.dart @@ -0,0 +1,51 @@ +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 new file mode 100644 index 0000000..ba9f89c --- /dev/null +++ b/packages/flutter_shopping_cart/lib/src/widgets/default_shopping_cart_item.dart @@ -0,0 +1,134 @@ +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 new file mode 100644 index 0000000..f597e29 --- /dev/null +++ b/packages/flutter_shopping_cart/lib/src/widgets/default_sum_bottom_sheet_builder.dart @@ -0,0 +1,42 @@ +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 index 0c57633..ebe32c0 100644 --- a/packages/flutter_shopping_cart/lib/src/widgets/product_item_popup.dart +++ b/packages/flutter_shopping_cart/lib/src/widgets/product_item_popup.dart @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.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 { @@ -49,7 +50,7 @@ class ProductItemPopup extends StatelessWidget { vertical: 8.0, ), child: Text( - configuration.localizations.close, + 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 index 4ffb093..3337de1 100644 --- a/packages/flutter_shopping_cart/lib/src/widgets/shopping_cart_screen.dart +++ b/packages/flutter_shopping_cart/lib/src/widgets/shopping_cart_screen.dart @@ -1,8 +1,12 @@ import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.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 StatelessWidget { +class ShoppingCartScreen extends StatefulWidget { /// Creates a shopping cart screen. const ShoppingCartScreen({ required this.configuration, @@ -10,86 +14,85 @@ class ShoppingCartScreen extends StatelessWidget { }); /// Configuration for the shopping cart screen. - final ShoppingCartConfig configuration; + final ShoppingCartConfig configuration; + @override + State createState() => _ShoppingCartScreenState(); +} + +class _ShoppingCartScreenState extends State { @override Widget build(BuildContext context) { var theme = Theme.of(context); - var productBuilder = SingleChildScrollView( - child: Column( - children: [ - if (configuration.titleBuilder != null) ...{ - configuration.titleBuilder!(context), - } else ...{ - Padding( - padding: const EdgeInsets.symmetric( - vertical: 32, - ), - child: Row( - children: [ - Text( - configuration.localizations.cartTitle, - style: theme.textTheme.titleLarge, - textAlign: TextAlign.start, - ), - ], - ), - ), - }, - ListenableBuilder( - listenable: configuration.productService, - builder: (context, _) { - var products = configuration.productService.products; - - if (products.isEmpty) { - return configuration.noContentBuilder(context); - } - - return Column( - children: [ - for (var product in products) - configuration.productItemBuilder( - context, - configuration.localizations.locale, - product, - configuration.productService, - configuration, - ), - // Additional whitespace at the bottom to make sure the - // last product(s) are not hidden by the bottom sheet. - SizedBox( - height: configuration.confirmOrderButtonHeight + - configuration.sumBottomSheetHeight, - ), - ], - ); - }, - ), - ], - ), - ); - return Scaffold( - appBar: configuration.appBar ?? - AppBar( - title: Text( - "Shopping cart", - style: theme.textTheme.headlineLarge, - ), - ), + appBar: widget.configuration.appBarBuilder?.call(context) ?? + const DefaultAppbar(), body: SafeArea( child: Stack( fit: StackFit.expand, children: [ Padding( - padding: configuration.padding, - child: productBuilder, + 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: configuration, + child: _BottomSheet( + configuration: widget.configuration, ), ), ], @@ -99,124 +102,30 @@ class ShoppingCartScreen extends StatelessWidget { } } -class _BottomSheet extends StatelessWidget { +class _BottomSheet extends StatelessWidget { const _BottomSheet({ required this.configuration, - super.key, - }); - - final ShoppingCartConfig configuration; - - @override - Widget build(BuildContext context) { - var placeOrderButton = ListenableBuilder( - listenable: configuration.productService, - builder: (BuildContext context, Widget? child) => - configuration.confirmOrderButtonBuilder != null - ? configuration.confirmOrderButtonBuilder!(context) - : _DefaultConfirmOrderButton(configuration: configuration), - ); - - var bottomSheet = ListenableBuilder( - listenable: configuration.productService, - builder: (BuildContext context, Widget? child) => - configuration.sumBottomSheetBuilder != null - ? configuration.sumBottomSheetBuilder!(context) - : _DefaultSumBottomSheet(configuration: configuration), - ); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - bottomSheet, - placeOrderButton, - ], - ); - } -} - -class _DefaultConfirmOrderButton extends StatelessWidget { - const _DefaultConfirmOrderButton({ - required this.configuration, - }); - - final ShoppingCartConfig configuration; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - void onConfirmOrderPressed(List products) { - if (configuration.onConfirmOrder == null) { - return; - } - - if (products.isEmpty) { - return; - } - - configuration.onConfirmOrder!(products); - } - - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 60), - child: SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: () => onConfirmOrderPressed( - configuration.productService.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.localizations.placeOrder, - style: theme.textTheme.displayLarge, - ), - ), - ), - ), - ); - } -} - -class _DefaultSumBottomSheet extends StatelessWidget { - const _DefaultSumBottomSheet({ - required this.configuration, }); final ShoppingCartConfig configuration; @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - var totalPrice = configuration.productService.products - .map((product) => product.price * product.quantity) - .fold(0.0, (a, b) => a + b); - - return Padding( - padding: configuration.bottomPadding, - child: Row( + Widget build(BuildContext context) => Column( + mainAxisSize: MainAxisSize.min, children: [ - Text( - configuration.localizations.sum, - style: theme.textTheme.titleMedium, - ), - const Spacer(), - Text( - "€ ${totalPrice.toStringAsFixed(2)}", - style: theme.textTheme.bodyMedium, - ), + 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 index 1a4ad49..d8860e9 100644 --- a/packages/flutter_shopping_cart/pubspec.yaml +++ b/packages/flutter_shopping_cart/pubspec.yaml @@ -1,22 +1,21 @@ name: flutter_shopping_cart description: "A Flutter module for a shopping cart." version: 2.0.0 -publish_to: 'none' +publish_to: "none" environment: - sdk: '>=3.3.0 <4.0.0' + sdk: ">=3.3.0 <4.0.0" flutter: ">=1.17.0" dependencies: flutter: sdk: flutter - flutter_shopping: + flutter_shopping_interface: git: url: https://github.com/Iconica-Development/flutter_shopping - path: packages/flutter_shopping + path: packages/flutter_shopping_interface ref: 2.0.0 - dev_dependencies: flutter_test: sdk: flutter @@ -25,4 +24,4 @@ dev_dependencies: url: https://github.com/Iconica-Development/flutter_iconica_analysis ref: 7.0.0 -flutter: \ No newline at end of file +flutter: diff --git a/packages/flutter_shopping_interface/lib/src/service/order_service.dart b/packages/flutter_shopping_interface/lib/src/service/order_service.dart index b3a1dab..2904d0d 100644 --- a/packages/flutter_shopping_interface/lib/src/service/order_service.dart +++ b/packages/flutter_shopping_interface/lib/src/service/order_service.dart @@ -5,8 +5,8 @@ import "package:flutter_shopping_interface/src/model/product.dart"; abstract class OrderService { /// Create an order Future createOrder( - int shopId, + String shopId, List products, - Map clientInformation, + 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 index 80bc20f..82abee1 100644 --- a/packages/flutter_shopping_interface/lib/src/service/product_service.dart +++ b/packages/flutter_shopping_interface/lib/src/service/product_service.dart @@ -11,4 +11,13 @@ abstract class ProductService with ChangeNotifier { /// 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/shopping_cart_service.dart b/packages/flutter_shopping_interface/lib/src/service/shopping_cart_service.dart index 27fabc3..6c2ce51 100644 --- a/packages/flutter_shopping_interface/lib/src/service/shopping_cart_service.dart +++ b/packages/flutter_shopping_interface/lib/src/service/shopping_cart_service.dart @@ -17,4 +17,7 @@ abstract class ShoppingCartService with ChangeNotifier { /// Clears the shopping cart. void clear(); + + /// The list of products in the shopping cart. + List get products; } diff --git a/packages/flutter_shopping_local/lib/service/local_order_service.dart b/packages/flutter_shopping_local/lib/service/local_order_service.dart index 190e74f..3f84868 100644 --- a/packages/flutter_shopping_local/lib/service/local_order_service.dart +++ b/packages/flutter_shopping_local/lib/service/local_order_service.dart @@ -5,11 +5,11 @@ import "package:flutter_shopping_interface/flutter_shopping_interface.dart"; class LocalOrderService with ChangeNotifier implements OrderService { @override Future createOrder( - int shopId, + String shopId, List products, - Map clientInformation, - ) { - // No use case for this method yet - throw UnimplementedError(); + 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 index e276743..bf67bef 100644 --- a/packages/flutter_shopping_local/lib/service/local_product_service.dart +++ b/packages/flutter_shopping_local/lib/service/local_product_service.dart @@ -4,10 +4,12 @@ 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() => - _products.map((e) => e.category).toSet().toList(); + _allProducts.map((e) => e.category).toSet().toList(); @override Future getProduct(String id) => @@ -60,6 +62,42 @@ class LocalProductService with ChangeNotifier implements ProductService { 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_shopping_cart_service.dart b/packages/flutter_shopping_local/lib/service/local_shopping_cart_service.dart index dafa160..d6c944f 100644 --- a/packages/flutter_shopping_local/lib/service/local_shopping_cart_service.dart +++ b/packages/flutter_shopping_local/lib/service/local_shopping_cart_service.dart @@ -54,4 +54,7 @@ class LocalShoppingCartService } notifyListeners(); } + + @override + List get products => _products; } diff --git a/packages/flutter_shopping_local/lib/service/service.dart b/packages/flutter_shopping_local/lib/service/service.dart index e974132..de7f343 100644 --- a/packages/flutter_shopping_local/lib/service/service.dart +++ b/packages/flutter_shopping_local/lib/service/service.dart @@ -1 +1,5 @@ +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";