diff --git a/packages/flutter_order_details/lib/flutter_order_details.dart b/packages/flutter_order_details/lib/flutter_order_details.dart index 0694d28..9bc1b8b 100644 --- a/packages/flutter_order_details/lib/flutter_order_details.dart +++ b/packages/flutter_order_details/lib/flutter_order_details.dart @@ -3,15 +3,6 @@ library flutter_order_details; export "src/configuration/order_detail_configuration.dart"; export "src/configuration/order_detail_localization.dart"; -export "src/configuration/order_detail_step.dart"; export "src/configuration/order_detail_title_style.dart"; -export "src/models/order_address_input.dart"; -export "src/models/order_choice_input.dart"; -export "src/models/order_dropdown_input.dart"; -export "src/models/order_email_input.dart"; -export "src/models/order_input.dart"; -export "src/models/order_phone_input.dart"; export "src/models/order_result.dart"; -export "src/models/order_text_input.dart"; -export "src/models/order_time_picker_input.dart"; export "src/widgets/order_detail_screen.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 1aaa75f..870d5cb 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,50 +1,537 @@ -import "package:flutter/widgets.dart"; -import "package:flutter_order_details/src/configuration/order_detail_localization.dart"; -import "package:flutter_order_details/src/configuration/order_detail_step.dart"; -import "package:flutter_order_details/src/models/order_result.dart"; +// 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"; /// Configuration for the order detail screen. class OrderDetailConfiguration { /// Constructor for the order detail configuration. - const OrderDetailConfiguration({ - required this.steps, - // + OrderDetailConfiguration({ required this.onCompleted, - // - this.progressIndicator = true, - // + this.pages = _defaultPages, this.localization = const OrderDetailLocalization(), - // - this.inputFieldPadding = const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, - ), - this.titlePadding = const EdgeInsets.only(left: 16, right: 16, top: 16), - // - this.appBar, + this.appBar = _defaultAppBar, + this.nextbuttonBuilder = _defaultNextButtonBuilder, }); /// 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 steps; + 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(OrderResult result) onCompleted; - - /// Whether or not you want to show a progress indicator at - /// the top of the screen. - final bool progressIndicator; + final Function(dynamic value) onCompleted; /// Localization for the order detail screen. final OrderDetailLocalization localization; - /// Padding around the input fields. - final EdgeInsets inputFieldPadding; - - /// Padding around the title of the input fields. - final EdgeInsets titlePadding; - /// Optional app bar that you can pass to the order detail screen. - final PreferredSizeWidget? appBar; + final AppBar Function( + BuildContext context, + OrderDetailLocalization localizations, + ) appBar; + + /// Optional next button builder that you can pass to the order detail screen. + final Widget Function( + int a, + // ignore: avoid_positional_boolean_parameters + bool b, + 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", + "Place another order", + ]; + return Align( + alignment: Alignment.bottomCenter, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 48), + 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: 8.0, + ), + 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", + ), + ], + ), + ), + ), + ]; } diff --git a/packages/flutter_order_details/lib/src/configuration/order_detail_localization.dart b/packages/flutter_order_details/lib/src/configuration/order_detail_localization.dart index f339d3a..df8c48c 100644 --- a/packages/flutter_order_details/lib/src/configuration/order_detail_localization.dart +++ b/packages/flutter_order_details/lib/src/configuration/order_detail_localization.dart @@ -2,17 +2,17 @@ class OrderDetailLocalization { /// Constructor for the order detail localization. const OrderDetailLocalization({ - this.nextButton = "Next", - this.backButton = "Back", + this.nextButton = "Order", this.completeButton = "Complete", + this.orderDetailsTitle = "Information", }); /// Next button localization. final String nextButton; - /// Back button localization. - final String backButton; - /// Complete button localization. final String completeButton; + + /// Title for the order details page. + final String orderDetailsTitle; } diff --git a/packages/flutter_order_details/lib/src/configuration/order_detail_step.dart b/packages/flutter_order_details/lib/src/configuration/order_detail_step.dart deleted file mode 100644 index feb0658..0000000 --- a/packages/flutter_order_details/lib/src/configuration/order_detail_step.dart +++ /dev/null @@ -1,22 +0,0 @@ -import "package:flutter/widgets.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; - -/// Configuration for the order detail step. -class OrderDetailStep { - /// Constructor for the order detail step. - OrderDetailStep({ - required this.formKey, - required this.fields, - this.stepName, - }); - - /// Optional name for the step. - final String? stepName; - - /// Form key for the step. - final GlobalKey formKey; - - /// List of fields that the user has to fill in. - /// Each field must extend from the `OrderDetailInput` class. - final List fields; -} diff --git a/packages/flutter_order_details/lib/src/models/formfield_error_builder.dart b/packages/flutter_order_details/lib/src/models/formfield_error_builder.dart deleted file mode 100644 index b764eed..0000000 --- a/packages/flutter_order_details/lib/src/models/formfield_error_builder.dart +++ /dev/null @@ -1,25 +0,0 @@ -import "package:flutter/material.dart"; - -/// Error Builder for form fields. -class FormFieldErrorBuilder extends StatelessWidget { - /// Constructor for the form field error builder. - const FormFieldErrorBuilder({ - required this.errorMessage, - super.key, - }); - - /// Error message to display. - final String errorMessage; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - return Text( - errorMessage, - textAlign: TextAlign.left, - style: TextStyle( - color: theme.colorScheme.error, - ), - ); - } -} diff --git a/packages/flutter_order_details/lib/src/models/order_address_input.dart b/packages/flutter_order_details/lib/src/models/order_address_input.dart deleted file mode 100644 index e34f4cc..0000000 --- a/packages/flutter_order_details/lib/src/models/order_address_input.dart +++ /dev/null @@ -1,160 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter/services.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; - -/// Order input for addresses with with predefined text fields and validation. -class OrderAddressInput extends OrderDetailInput { - /// Constructor of the order address input. - OrderAddressInput({ - required super.title, - required super.outputKey, - required this.textController, - super.titleStyle, - super.titleAlignment, - super.titlePadding, - super.subtitle, - super.errorIsRequired, - super.hint = "0000XX", - super.isRequired, - super.isReadOnly, - super.initialValue, - this.streetNameTitle = "Street name", - this.postalCodeTitle = "Postal code", - this.cityTitle = "City", - this.streetNameValidators, - this.postalCodeValidators, - this.cityValidators, - this.inputFormatters, - super.paddingBetweenFields = const EdgeInsets.symmetric(vertical: 4), - }); - - /// Title for the street name. - final String streetNameTitle; - - /// Title for the postal code. - final String postalCodeTitle; - - /// Title for the city. - final String cityTitle; - - /// Text Control parent that contains the value of all the other three - /// controllers. - final TextEditingController textController; - - /// Text Controller for street names. - final TextEditingController streetNameController = TextEditingController(); - - /// Text Controller for postal codes. - final TextEditingController postalCodeController = TextEditingController(); - - /// Text Controller for the city name. - final TextEditingController cityController = TextEditingController(); - - /// Validators for the street name. - final List? streetNameValidators; - - /// Validators for the postal code. - final List? postalCodeValidators; - - /// Validators for the city. - final List? cityValidators; - - /// Input formatters for the postal code. - final List? inputFormatters; - - @override - Widget build( - BuildContext context, - String? buildInitialValue, - Function({bool needsBlur}) onBlurBackground, - ) { - void setUpControllers(String address) { - var addressParts = address.split(", "); - - if (addressParts.isNotEmpty) { - streetNameController.text = addressParts[0]; - } - - if (addressParts.length > 1) { - postalCodeController.text = addressParts[1]; - } - - if (addressParts.length > 2) { - cityController.text = addressParts[2]; - } - } - - void inputChanged(String _) { - var address = "${streetNameController.text}, " - "${postalCodeController.text}, " - "${cityController.text}"; - - textController.text = address; - - currentValue = address; - onValueChanged?.call(address); - } - - textController.text = initialValue ?? buildInitialValue ?? ""; - currentValue = textController.text; - - setUpControllers(currentValue ?? ""); - - return buildOutline( - context, - [ - OrderTextInput( - title: streetNameTitle, - outputKey: "internal_street_name", - textController: streetNameController, - titleStyle: OrderDetailTitleStyle.none, - onValueChanged: inputChanged, - hint: "De Dam 1", - initialValue: streetNameController.text, - validators: streetNameValidators ?? [], - ), - OrderTextInput( - title: postalCodeTitle, - outputKey: "internal_postal_code", - textController: postalCodeController, - titleStyle: OrderDetailTitleStyle.none, - onValueChanged: inputChanged, - validators: postalCodeValidators ?? - [ - (value) { - if (value?.length != 6) { - return "Postal code must be 6 characters"; - } - return null; - }, - (value) { - if (value != null && - !RegExp(r"^\d{4}\s?[a-zA-Z]{2}$").hasMatch(value)) { - return "Postal code must be in the format 0000XX"; - } - return null; - } - ], - inputFormatters: inputFormatters ?? - [ - FilteringTextInputFormatter.allow(RegExp(r"^\d{0,4}[A-Z]*")), - LengthLimitingTextInputFormatter(6), - ], - hint: hint, - initialValue: postalCodeController.text, - ), - OrderTextInput( - title: cityTitle, - outputKey: "internal_city", - textController: cityController, - titleStyle: OrderDetailTitleStyle.none, - onValueChanged: inputChanged, - hint: "Amsterdam", - initialValue: cityController.text, - validators: cityValidators ?? [], - ), - ], - onBlurBackground, - ); - } -} diff --git a/packages/flutter_order_details/lib/src/models/order_choice_input.dart b/packages/flutter_order_details/lib/src/models/order_choice_input.dart deleted file mode 100644 index f482233..0000000 --- a/packages/flutter_order_details/lib/src/models/order_choice_input.dart +++ /dev/null @@ -1,175 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; -import "package:flutter_order_details/src/models/formfield_error_builder.dart"; - -/// Order input for choice with predefined text fields and validation. -class OrderChoiceInput extends OrderDetailInput { - /// Constructor of the order choice input. - OrderChoiceInput({ - required super.title, - required super.outputKey, - required this.items, - super.titleStyle, - super.titleAlignment, - super.titlePadding, - super.subtitle, - super.errorIsRequired, - super.isRequired, - super.isReadOnly, - super.initialValue, - this.fieldHeight = 140, - this.fieldPadding = const EdgeInsets.symmetric( - horizontal: 4, - vertical: 64, - ), - this.paddingBetweenFields = const EdgeInsets.symmetric(vertical: 12), - }); - - /// Items to show within the dropdown menu. - final List items; - - /// Padding for the field. - final EdgeInsets fieldPadding; - - /// Padding between fields. - @override - // ignore: overridden_fields - final EdgeInsets paddingBetweenFields; - - /// The height of the input field. - final double fieldHeight; - - final _ChoiceNotifier _notifier = _ChoiceNotifier(); - - @override - Widget build( - BuildContext context, - String? buildInitialValue, - Function({bool needsBlur}) onBlurBackground, - ) { - void onItemChanged(String value) { - if (value == currentValue) { - currentValue = null; - onValueChanged?.call(""); - _notifier.setValue(""); - } else { - currentValue = value; - onValueChanged?.call(value); - _notifier.setValue(value); - } - } - - return buildOutline( - context, - ListenableBuilder( - listenable: _notifier, - builder: (context, child) => _ChoiceInputField( - currentValue: currentValue ?? initialValue ?? buildInitialValue ?? "", - items: items, - onTap: onItemChanged, - validate: validate, - fieldPadding: fieldPadding, - paddingBetweenFields: paddingBetweenFields, - ), - ), - onBlurBackground, - ); - } -} - -class _ChoiceNotifier extends ChangeNotifier { - String? _value; - - String? get value => _value; - - void setValue(String value) { - _value = value; - notifyListeners(); - } -} - -class _ChoiceInputField extends FormField { - _ChoiceInputField({ - required T currentValue, - required List items, - required Function(T) onTap, - required String? Function(T?) validate, - required EdgeInsets fieldPadding, - required EdgeInsets paddingBetweenFields, - super.key, - }) : super( - validator: (value) => validate(currentValue), - builder: (FormFieldState field) => Padding( - padding: fieldPadding, - child: Column( - children: [ - for (var item in items) ...[ - Padding( - padding: paddingBetweenFields, - child: _InputContent( - i: item, - currentValue: currentValue, - onTap: onTap, - ), - ), - ], - if (field.hasError) ...[ - FormFieldErrorBuilder(errorMessage: field.errorText!), - ], - ], - ), - ), - ); -} - -class _InputContent extends StatelessWidget { - const _InputContent({ - required this.i, - required this.currentValue, - required this.onTap, - }); - - final T i; - final T currentValue; - final Function(T) onTap; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - var boxDecoration = BoxDecoration( - color: currentValue == i.toString() - ? theme.colorScheme.primary - : theme.colorScheme.secondary, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: theme.colorScheme.primary, - width: 1, - ), - ); - - var decoratedBox = Container( - decoration: boxDecoration, - width: double.infinity, - height: 150, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - i.toString(), - style: theme.textTheme.labelLarge?.copyWith( - color: currentValue == i.toString() - ? theme.colorScheme.onPrimary - : theme.colorScheme.primary, - ), - ), - ], - ), - ); - - return GestureDetector( - onTap: () => onTap(i), - child: decoratedBox, - ); - } -} diff --git a/packages/flutter_order_details/lib/src/models/order_dropdown_input.dart b/packages/flutter_order_details/lib/src/models/order_dropdown_input.dart deleted file mode 100644 index 640099e..0000000 --- a/packages/flutter_order_details/lib/src/models/order_dropdown_input.dart +++ /dev/null @@ -1,153 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; - -/// Order Detail input for a dropdown input. -class OrderDropdownInput extends OrderDetailInput { - /// Constructor for the order dropdown input. - OrderDropdownInput({ - required super.title, - required super.outputKey, - required this.items, - super.titleStyle, - super.titleAlignment, - super.titlePadding, - super.subtitle, - super.errorIsRequired, - super.isRequired = true, - super.isReadOnly, - super.initialValue, - this.blurOnInteraction = true, - }); - - /// Items to show within the dropdown menu. - final List items; - - /// Whether or not the screen should blur when interacting. - final bool blurOnInteraction; - - @override - Widget build( - BuildContext context, - T? buildInitialValue, - Function({bool needsBlur}) onBlurBackground, - ) { - var theme = Theme.of(context); - - void onItemChanged(T? value) { - currentValue = value; - onValueChanged?.call(value as T); - onBlurBackground(needsBlur: false); - } - - void onPopupOpen() { - if (blurOnInteraction) - onBlurBackground( - needsBlur: true, - ); - } - - var inputDecoration = InputDecoration( - labelText: titleStyle == OrderDetailTitleStyle.label ? title : null, - hintText: hint, - filled: true, - fillColor: theme.inputDecorationTheme.fillColor, - border: InputBorder.none, - ); - - currentValue = - currentValue ?? initialValue ?? buildInitialValue ?? items[0]; - - return buildOutline( - context, - DropdownButtonFormField( - value: currentValue ?? initialValue ?? buildInitialValue ?? items[0], - selectedItemBuilder: (context) => items - .map( - (item) => Text( - item.toString(), - style: theme.textTheme.labelMedium, - ), - ) - .toList(), - items: items - .map( - (item) => DropdownMenuItem( - value: item, - child: _DropdownButtonBuilder( - item: item, - currentValue: currentValue, - ), - ), - ) - .toList(), - onChanged: onItemChanged, - onTap: onPopupOpen, - style: theme.textTheme.labelMedium, - decoration: inputDecoration, - borderRadius: BorderRadius.circular(10), - icon: const Icon(Icons.keyboard_arrow_down_sharp), - validator: super.validate, - ), - onBlurBackground, - ); - } -} - -class _DropdownButtonBuilder extends StatelessWidget { - const _DropdownButtonBuilder({ - required this.item, - this.currentValue, - super.key, - }); - - final T item; - final T? currentValue; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - var textBuilder = Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: const EdgeInsets.all(8), - child: Text( - item.toString(), - style: theme.textTheme.labelMedium?.copyWith( - color: item == currentValue ? theme.colorScheme.onPrimary : null, - fontWeight: FontWeight.w500, - ), - ), - ), - ); - - var selectedIcon = Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(8), - child: Icon( - Icons.check, - color: theme.colorScheme.onPrimary, - ), - ), - ); - - return DecoratedBox( - decoration: BoxDecoration( - color: item == currentValue ? theme.colorScheme.primary : null, - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: theme.colorScheme.primary, - ), - ), - child: Stack( - children: [ - textBuilder, - if (currentValue == item) ...[ - selectedIcon, - ], - ], - ), - ); - } -} diff --git a/packages/flutter_order_details/lib/src/models/order_email_input.dart b/packages/flutter_order_details/lib/src/models/order_email_input.dart deleted file mode 100644 index a2bf053..0000000 --- a/packages/flutter_order_details/lib/src/models/order_email_input.dart +++ /dev/null @@ -1,75 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; - -/// Order Email input with predefined validators. -class OrderEmailInput extends OrderDetailInput { - /// Constructor of the order email input. - OrderEmailInput({ - required super.title, - required super.outputKey, - required this.textController, - super.titleStyle, - super.titleAlignment, - super.titlePadding, - super.subtitle, - super.hint, - super.errorIsRequired, - super.isRequired, - super.isReadOnly, - super.initialValue, - this.errorInvalidEmail = "Invalid email ( your_name@example.com )", - }) : super( - validators: [ - (value) { - if (value != null && !RegExp(r"^\w+@\w+\.\w+$").hasMatch(value)) { - return errorInvalidEmail; - } - return null; - }, - ], - ); - - /// Text Controller for email input. - final TextEditingController textController; - - /// Error message for invalid email. - final String errorInvalidEmail; - - @override - Widget build( - BuildContext context, - String? buildInitialValue, - Function({bool needsBlur}) onBlurBackground, - ) { - var theme = Theme.of(context); - - textController.text = initialValue ?? buildInitialValue ?? ""; - currentValue = textController.text; - - return buildOutline( - context, - TextFormField( - style: theme.textTheme.labelMedium, - controller: textController, - onChanged: (String value) { - currentValue = value; - super.onValueChanged?.call(value); - }, - decoration: InputDecoration( - labelText: titleStyle == OrderDetailTitleStyle.label ? title : null, - hintText: hint, - filled: true, - fillColor: theme.inputDecorationTheme.fillColor, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide.none, - ), - ), - validator: (value) => super.validate(value), - keyboardType: TextInputType.emailAddress, - readOnly: isReadOnly, - ), - onBlurBackground, - ); - } -} diff --git a/packages/flutter_order_details/lib/src/models/order_phone_input.dart b/packages/flutter_order_details/lib/src/models/order_phone_input.dart deleted file mode 100644 index f6afb3a..0000000 --- a/packages/flutter_order_details/lib/src/models/order_phone_input.dart +++ /dev/null @@ -1,101 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter/services.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; - -/// Order input for phone numbers with with predefined -/// text fields and validation. -class OrderPhoneInput extends OrderDetailInput { - /// Constructor for the phone input. - OrderPhoneInput({ - required super.title, - required super.outputKey, - required this.textController, - this.errorMustBe11Digits = "Number must be 11 digits (+31 6 XXXX XXXX)", - this.errorMustStartWith316 = "Number must start with +316", - this.errorMustBeNumeric = "Number must be numeric", - super.errorIsRequired, - super.subtitle, - super.titleAlignment, - super.titlePadding, - super.titleStyle, - super.isRequired, - super.isReadOnly, - super.initialValue, - }) : super( - validators: [ - (value) { - if (value != null && value.length != 11) { - return errorMustBe11Digits; - } - return null; - }, - (value) { - if (value != null && !value.startsWith("316")) { - return errorMustStartWith316; - } - return null; - }, - (value) { - if (value != null && !RegExp(r"^\d+$").hasMatch(value)) { - return errorMustBeNumeric; - } - return null; - }, - ], - ); - - /// Text Controller for phone input. - final TextEditingController textController; - - /// Error message that notifies the number must be 11 digits long. - final String errorMustBe11Digits; - - /// Error message that notifies the number must start with +316 - final String errorMustStartWith316; - - /// Error message that notifies the number must be numeric. - final String errorMustBeNumeric; - - @override - Widget build( - BuildContext context, - String? buildInitialValue, - Function({bool needsBlur}) onBlurBackground, - ) { - var theme = Theme.of(context); - - textController.text = initialValue ?? buildInitialValue ?? "31"; - currentValue = textController.text; - - return buildOutline( - context, - TextFormField( - style: theme.textTheme.labelMedium, - controller: textController, - onChanged: (String value) { - currentValue = value; - super.onValueChanged?.call(value); - }, - decoration: InputDecoration( - labelText: titleStyle == OrderDetailTitleStyle.label ? title : null, - prefixText: "+", - prefixStyle: theme.textTheme.labelMedium, - hintText: hint, - filled: true, - fillColor: theme.inputDecorationTheme.fillColor, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide.none, - ), - ), - validator: (value) => super.validate(value), - readOnly: isReadOnly, - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - LengthLimitingTextInputFormatter(11), // international phone number - ], - ), - onBlurBackground, - ); - } -} diff --git a/packages/flutter_order_details/lib/src/models/order_text_input.dart b/packages/flutter_order_details/lib/src/models/order_text_input.dart deleted file mode 100644 index 5967160..0000000 --- a/packages/flutter_order_details/lib/src/models/order_text_input.dart +++ /dev/null @@ -1,71 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter/services.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; - -/// Default text input for order details. -class OrderTextInput extends OrderDetailInput { - /// Default text input for order details. - OrderTextInput({ - required super.title, - required super.outputKey, - required this.textController, - super.titleStyle, - super.titleAlignment, - super.titlePadding, - super.subtitle, - super.isRequired, - super.isReadOnly, - super.initialValue, - super.validators, - super.onValueChanged, - super.errorIsRequired, - super.hint, - this.inputFormatters = const [], - }); - - /// Text Controller for the input field. - final TextEditingController textController; - - /// List of input formatters for the text field. - final List inputFormatters; - - @override - Widget build( - BuildContext context, - String? buildInitialValue, - Function({bool needsBlur}) onBlurBackground, - ) { - var theme = Theme.of(context); - - textController.text = initialValue ?? buildInitialValue ?? ""; - currentValue = textController.text; - - return buildOutline( - context, - TextFormField( - style: theme.textTheme.labelMedium, - controller: textController, - onChanged: (String value) { - currentValue = value; - super.onValueChanged?.call(value); - }, - decoration: InputDecoration( - labelText: titleStyle == OrderDetailTitleStyle.label ? title : null, - hintText: hint, - filled: true, - fillColor: theme.inputDecorationTheme.fillColor, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide.none, - ), - ), - validator: super.validate, - readOnly: isReadOnly, - inputFormatters: [ - ...inputFormatters, - ], - ), - onBlurBackground, - ); - } -} diff --git a/packages/flutter_order_details/lib/src/models/order_time_picker_input.dart b/packages/flutter_order_details/lib/src/models/order_time_picker_input.dart deleted file mode 100644 index b8ef05f..0000000 --- a/packages/flutter_order_details/lib/src/models/order_time_picker_input.dart +++ /dev/null @@ -1,353 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_order_details/flutter_order_details.dart"; -import "package:flutter_order_details/src/models/formfield_error_builder.dart"; - -/// Order time picker input with predefined text fields and validation. -class OrderTimePicker extends OrderDetailInput { - /// Constructor for the time picker. - OrderTimePicker({ - required super.title, - required super.outputKey, - super.titleStyle, - super.titleAlignment, - super.titlePadding, - super.subtitle, - super.isRequired, - super.initialValue, - super.validators, - super.onValueChanged, - super.errorIsRequired, - super.hint, - this.beginTime = 9, - this.endTime = 17, - this.interval = 0.25, - this.morningLabel = "Morning", - this.afternoonLabel = "Afternoon", - this.eveningLabel = "Evening", - this.padding = const EdgeInsets.only(top: 12, bottom: 20.0), - }) : assert( - beginTime < endTime, - "Begin time cannot be greater than end time", - ); - - /// Minimum time of times to show. For example 9 (for 9AM). - final double beginTime; - - /// Final time to show. For example 17 (for 5PM). - final double endTime; - - /// For each interval a button gets generated within the begin time and - /// the end time. For example 0.25 (for ever 15 minutes). - final double interval; - - /// Translation for morning texts. - final String morningLabel; - - /// Translation for afternoon texts. - final String afternoonLabel; - - /// Translation for evening texts. - final String eveningLabel; - - /// Padding around the time picker. - final EdgeInsets padding; - - final _selectedTimeOfDay = _SelectedTimeOfDay(); - - @override - Widget build( - BuildContext context, - String? buildInitialValue, - Function({bool needsBlur}) onBlurBackground, - ) { - void updateSelectedTimeOfDay(_TimeOfDay timeOfDay) { - if (_selectedTimeOfDay.selectedTimeOfDay == timeOfDay) return; - _selectedTimeOfDay.selectedTimeOfDay = timeOfDay; - currentValue = null; - } - - void updateSelectedTimeAsString(String? time) { - currentValue = time; - onValueChanged?.call(time ?? ""); - _selectedTimeOfDay.selectedTime = time; - } - - void updateSelectedTime(double time) { - if (currentValue == time.toString()) { - updateSelectedTimeAsString(null); - } else { - updateSelectedTimeAsString(time.toString()); - } - } - - if (currentValue != null) { - var currentValueAsDouble = double.parse(currentValue!); - for (var timeOfDay in _TimeOfDay.values) { - if (_isTimeWithinTimeOfDay( - currentValueAsDouble, - currentValueAsDouble, - timeOfDay, - )) { - _selectedTimeOfDay.selectedTimeOfDay = timeOfDay; - } - } - updateSelectedTimeAsString(currentValue); - } else { - for (var timeOfDay in _TimeOfDay.values) { - if (_isTimeWithinTimeOfDay(beginTime, endTime, timeOfDay)) { - _selectedTimeOfDay.selectedTimeOfDay = timeOfDay; - break; - } - } - } - - return buildOutline( - context, - ListenableBuilder( - listenable: _selectedTimeOfDay, - builder: (context, _) { - var startTime = _selectedTimeOfDay.selection != null - ? _selectedTimeOfDay.selection!.minTime.clamp(beginTime, endTime) - : beginTime; - var finalTime = _selectedTimeOfDay.selection != null - ? _selectedTimeOfDay.selection!.maxTime.clamp(beginTime, endTime) - : endTime; - - return Column( - children: [ - _TimeOfDaySelector( - selectedTimeOfDay: _selectedTimeOfDay, - updateSelectedTimeOfDay: updateSelectedTimeOfDay, - startTime: beginTime, - endTime: endTime, - morningLabel: morningLabel, - afternoonLabel: afternoonLabel, - eveningLabel: eveningLabel, - padding: padding, - ), - _TimeWrap( - currentValue: currentValue ?? "", - startTime: startTime, - finalTime: finalTime, - interval: interval, - onTap: updateSelectedTime, - validate: super.validate, - ), - ], - ); - }, - ), - onBlurBackground, - ); - } -} - -bool _isTimeWithinTimeOfDay( - double openingTime, - double closingTime, - _TimeOfDay timeOfDay, -) => - (timeOfDay.minTime >= openingTime && timeOfDay.minTime <= closingTime) || - (timeOfDay.maxTime > openingTime && timeOfDay.maxTime <= closingTime) || - (timeOfDay.minTime <= openingTime && timeOfDay.maxTime >= closingTime); - -class _TimeOfDaySelector extends StatelessWidget { - const _TimeOfDaySelector({ - required this.selectedTimeOfDay, - required this.updateSelectedTimeOfDay, - required this.startTime, - required this.endTime, - required this.morningLabel, - required this.afternoonLabel, - required this.eveningLabel, - required this.padding, - }); - - final _SelectedTimeOfDay selectedTimeOfDay; - final Function(_TimeOfDay) updateSelectedTimeOfDay; - final double startTime; - final double endTime; - final String morningLabel; - final String afternoonLabel; - final String eveningLabel; - final EdgeInsets padding; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - String getLabelName(_TimeOfDay timeOfDay) => switch (timeOfDay) { - _TimeOfDay.morning => morningLabel, - _TimeOfDay.afternoon => afternoonLabel, - _TimeOfDay.evening => eveningLabel, - }; - - return Padding( - padding: padding, - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(90), - border: Border.all( - color: theme.colorScheme.primary, - strokeAlign: BorderSide.strokeAlignOutside, - ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - for (var timeOfDay in _TimeOfDay.values) ...[ - if (_isTimeWithinTimeOfDay(startTime, endTime, timeOfDay)) ...[ - GestureDetector( - onTap: () => updateSelectedTimeOfDay(timeOfDay), - child: DecoratedBox( - decoration: BoxDecoration( - color: selectedTimeOfDay.selectedTimeOfDay == timeOfDay - ? theme.colorScheme.primary - : Colors.white, - borderRadius: BorderRadius.circular(90), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 8, - ), - child: Text( - getLabelName(timeOfDay), - style: theme.textTheme.labelMedium?.copyWith( - color: - selectedTimeOfDay.selectedTimeOfDay == timeOfDay - ? Colors.white - : theme.colorScheme.primary, - ), - ), - ), - ), - ), - ], - ], - ], - ), - ), - ); - } -} - -class _TimeWrap extends FormField { - _TimeWrap({ - required this.currentValue, - required this.startTime, - required this.finalTime, - required this.interval, - required this.onTap, - required String? Function(T?) validate, - }) : super( - validator: (value) => validate(currentValue), - builder: (FormFieldState field) => Column( - children: [ - Wrap( - children: [ - for (var i = startTime; i < finalTime; i += interval) ...[ - _TimeWrapContent( - i: i, - currentValue: currentValue, - onTap: onTap, - ), - ], - ], - ), - if (field.hasError) ...[ - FormFieldErrorBuilder(errorMessage: field.errorText!), - ], - ], - ), - ); - - final T currentValue; - final double startTime; - final double finalTime; - final double interval; - final Function(double) onTap; -} - -class _TimeWrapContent extends StatelessWidget { - const _TimeWrapContent({ - required this.i, - required this.currentValue, - required this.onTap, - }); - - final double i; - final T currentValue; - final Function(double) onTap; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - var boxDecoration = BoxDecoration( - color: currentValue == i.toString() - ? theme.colorScheme.primary - : Colors.white, - borderRadius: BorderRadius.circular(16), - ); - - var decoratedBox = Container( - decoration: boxDecoration, - width: MediaQuery.of(context).size.width * .25, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 28, - vertical: 12, - ), - child: Text( - '${i.floor().toString().padLeft(2, '0')}:' - '${((i - i.floor()) * 60).toInt().toString().padLeft(2, '0')}', - style: theme.textTheme.labelMedium?.copyWith( - color: currentValue == i.toString() - ? Colors.white - : theme.colorScheme.primary, - ), - ), - ), - ); - - return GestureDetector( - onTap: () => onTap(i), - child: Padding( - padding: const EdgeInsets.all(4.0), - child: decoratedBox, - ), - ); - } -} - -class _SelectedTimeOfDay extends ChangeNotifier { - _TimeOfDay? selection; - String? time = ""; - - _TimeOfDay? get selectedTimeOfDay => selection; - String? get selectedTime => time; - - set selectedTimeOfDay(_TimeOfDay? value) { - selection = value; - notifyListeners(); - } - - set selectedTime(String? value) { - time = value; - notifyListeners(); - } -} - -enum _TimeOfDay { - morning(0, 12), - afternoon(12, 18), - evening(18, 24); - - const _TimeOfDay(this.minTime, this.maxTime); - - final double minTime; - final double maxTime; -} 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 index 0304e0b..d24cd2a 100644 --- a/packages/flutter_order_details/lib/src/widgets/order_detail_screen.dart +++ b/packages/flutter_order_details/lib/src/widgets/order_detail_screen.dart @@ -1,4 +1,5 @@ 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. @@ -17,257 +18,29 @@ class OrderDetailScreen extends StatefulWidget { } class _OrderDetailScreenState extends State { - final _CurrentStep _currentStep = _CurrentStep(); - - final OrderResult _orderResult = OrderResult(order: {}); - - bool _blurBackground = false; - - void _toggleBlurBackground({bool? needsBlur}) { - setState(() { - _blurBackground = needsBlur!; - }); - } - @override Widget build(BuildContext context) { - var theme = Theme.of(context); - - var pageBody = SafeArea( - left: false, - right: false, - bottom: true, - child: _OrderDetailBody( - configuration: widget.configuration, - orderResult: _orderResult, - currentStep: _currentStep, - onBlurBackground: _toggleBlurBackground, - ), - ); - - var pageBlur = GestureDetector( - onTap: () => _toggleBlurBackground(needsBlur: false), - child: Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - color: theme.colorScheme.surface.withOpacity(0.5), - ), - ), - ); - + var controller = FlutterFormController(); return Scaffold( - appBar: widget.configuration.appBar, - body: Stack( - children: [ - pageBody, - if (_blurBackground) pageBlur, - ], + 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) {}, + ), ), ); } } - -class _CurrentStep extends ChangeNotifier { - int _step = 0; - - int get step => _step; - - void increment() { - _step++; - notifyListeners(); - } - - void decrement() { - _step--; - notifyListeners(); - } -} - -class _OrderDetailBody extends StatelessWidget { - const _OrderDetailBody({ - required this.configuration, - required this.orderResult, - required this.currentStep, - required this.onBlurBackground, - }); - - final OrderDetailConfiguration configuration; - final OrderResult orderResult; - final _CurrentStep currentStep; - final Function({bool needsBlur}) onBlurBackground; - - @override - Widget build(BuildContext context) => ListenableBuilder( - listenable: currentStep, - builder: (context, _) => Builder( - builder: (context) => _FormBuilder( - currentStep: currentStep, - orderResult: orderResult, - configuration: configuration, - onBlurBackground: onBlurBackground, - ), - ), - ); -} - -class _FormBuilder extends StatelessWidget { - const _FormBuilder({ - required this.currentStep, - required this.configuration, - required this.orderResult, - required this.onBlurBackground, - }); - - final _CurrentStep currentStep; - final OrderDetailConfiguration configuration; - final OrderResult orderResult; - - final Function({bool needsBlur}) onBlurBackground; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - var progressIndicator = LinearProgressIndicator( - value: currentStep.step / configuration.steps.length, - backgroundColor: theme.colorScheme.surface, - ); - - var stepForm = Form( - key: configuration.steps[currentStep.step].formKey, - child: _StepBuilder( - configuration: configuration, - currentStep: configuration.steps[currentStep.step], - orderResult: orderResult, - theme: theme, - onBlurBackground: onBlurBackground, - ), - ); - - void onPressedNext() { - var formInfo = configuration.steps[currentStep.step]; - var formkey = formInfo.formKey; - for (var input in formInfo.fields) { - orderResult.order[input.outputKey] = input.currentValue; - } - - if (formkey.currentState!.validate()) { - currentStep.increment(); - } - } - - void onPressedPrevious() { - var formInfo = configuration.steps[currentStep.step]; - for (var input in formInfo.fields) { - orderResult.order[input.outputKey] = input.currentValue; - } - - currentStep.decrement(); - } - - void onPressedComplete() { - var formInfo = configuration.steps[currentStep.step]; - var formkey = formInfo.formKey; - for (var input in formInfo.fields) { - orderResult.order[input.outputKey] = input.currentValue; - } - - if (formkey.currentState!.validate()) { - configuration.onCompleted(orderResult); - } - } - - var navigationControl = Row( - children: [ - if (currentStep.step > 0) ...[ - TextButton( - onPressed: onPressedPrevious, - child: Text( - configuration.localization.backButton, - ), - ), - ], - const Spacer(), - if (currentStep.step < configuration.steps.length - 1) ...[ - TextButton( - onPressed: onPressedNext, - child: Text( - configuration.localization.nextButton, - ), - ), - ] else ...[ - TextButton( - onPressed: onPressedComplete, - child: Text( - configuration.localization.completeButton, - ), - ), - ], - ], - ); - - return Stack( - children: [ - SingleChildScrollView( - child: Column( - children: [ - if (configuration.progressIndicator) ...[ - progressIndicator, - ], - stepForm, - ], - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: navigationControl, - ), - ], - ); - } -} - -class _StepBuilder extends StatelessWidget { - const _StepBuilder({ - required this.configuration, - required this.currentStep, - required this.orderResult, - required this.theme, - required this.onBlurBackground, - }); - - final OrderDetailConfiguration configuration; - final OrderDetailStep currentStep; - final OrderResult orderResult; - final ThemeData theme; - final Function({bool needsBlur}) onBlurBackground; - - @override - Widget build(BuildContext context) { - var title = currentStep.stepName != null - ? Padding( - padding: configuration.titlePadding, - child: Text( - currentStep.stepName!, - style: theme.textTheme.titleMedium, - ), - ) - : const SizedBox.shrink(); - - return Column( - children: [ - title, - for (var input in currentStep.fields) - Padding( - padding: configuration.inputFieldPadding, - child: input.build( - context, - orderResult.order[input.outputKey], - onBlurBackground, - ), - ), - ], - ); - } -} diff --git a/packages/flutter_product_page/lib/src/models/product.dart b/packages/flutter_product_page/lib/src/models/product.dart new file mode 100644 index 0000000..56ea5f7 --- /dev/null +++ b/packages/flutter_product_page/lib/src/models/product.dart @@ -0,0 +1,38 @@ +/// The product page shop class contains all the required information +class Product { + /// Constructor for the product. + Product({ + required this.id, + required this.name, + required this.imageUrl, + required this.category, + required this.price, + required this.hasDiscount, + 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. + final int quantity; +} diff --git a/packages/flutter_shopping/example/lib/src/configuration/configuration.dart b/packages/flutter_shopping/example/lib/src/configuration/configuration.dart index 9184c8b..3ab711c 100644 --- a/packages/flutter_shopping/example/lib/src/configuration/configuration.dart +++ b/packages/flutter_shopping/example/lib/src/configuration/configuration.dart @@ -34,7 +34,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() => ), // (REQUIRED): Function to navigate to the shopping cart - onNavigateToShoppingCart: () => onCompleteProductPage(context), + 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 @@ -116,7 +116,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() => // (OPTIONAL/REQUIRED) on confirm order callback: // Either use this callback or the placeOrderButtonBuilder. - onConfirmOrder: (products) => onCompleteShoppingCart(context), + onConfirmOrder: (products) async => onCompleteShoppingCart(context), // (RECOMMENDED) localizations: localizations: const ShoppingCartLocalizations(), diff --git a/packages/flutter_shopping/lib/src/config/default_order_detail_configuration.dart b/packages/flutter_shopping/lib/src/config/default_order_detail_configuration.dart deleted file mode 100644 index 67dfd81..0000000 --- a/packages/flutter_shopping/lib/src/config/default_order_detail_configuration.dart +++ /dev/null @@ -1,68 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_shopping/flutter_shopping.dart"; -import "package:go_router/go_router.dart"; - -/// Default order detail configuration for the app. -/// This configuration is used to create the order detail page. -OrderDetailConfiguration getDefaultOrderDetailConfiguration( - BuildContext context, - FlutterShoppingConfiguration configuration, -) => - OrderDetailConfiguration( - steps: [ - OrderDetailStep( - formKey: GlobalKey(), - stepName: "Basic Information", - fields: [ - OrderTextInput( - title: "First name", - outputKey: "first_name", - textController: TextEditingController(), - ), - OrderTextInput( - title: "Last name", - outputKey: "last_name", - textController: TextEditingController(), - ), - OrderEmailInput( - title: "Your email address", - outputKey: "email", - textController: TextEditingController(), - subtitle: "* We will send your order confirmation here", - hint: "your_email@mail.com", - ), - ], - ), - OrderDetailStep( - formKey: GlobalKey(), - stepName: "Address Information", - fields: [ - OrderAddressInput( - title: "Your address", - outputKey: "address", - textController: TextEditingController(), - ), - ], - ), - OrderDetailStep( - formKey: GlobalKey(), - stepName: "Payment Information", - fields: [ - OrderChoiceInput( - title: "Payment option", - outputKey: "payment_option", - items: ["Pay now", "Pay later"], - ), - ], - ), - ], - onCompleted: (OrderResult result) async => - onCompleteOrderDetails(context, configuration, result), - appBar: AppBar( - title: const Text("Order Details"), - leading: IconButton( - icon: const Icon(Icons.close, color: Colors.white), - onPressed: () => context.go(FlutterShoppingPathRoutes.shoppingCart), - ), - ), - ); diff --git a/packages/flutter_shopping/lib/src/config/order_details_localizations.dart b/packages/flutter_shopping/lib/src/config/order_details_localizations.dart new file mode 100644 index 0000000..af50182 --- /dev/null +++ b/packages/flutter_shopping/lib/src/config/order_details_localizations.dart @@ -0,0 +1,10 @@ +/// 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/user_stores/flutter_shopping_userstory_go_router.dart b/packages/flutter_shopping/lib/src/user_stores/flutter_shopping_userstory_go_router.dart index 46ca675..1e8b165 100644 --- 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 @@ -1,5 +1,4 @@ import "package:flutter_shopping/flutter_shopping.dart"; -import "package:flutter_shopping/src/config/default_order_detail_configuration.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"; @@ -29,8 +28,11 @@ List getShoppingStoryRoutes({ builder: (context, state) => configuration.orderDetailsBuilder != null ? configuration.orderDetailsBuilder!(context) : OrderDetailScreen( - configuration: - getDefaultOrderDetailConfiguration(context, configuration), + configuration: OrderDetailConfiguration( + onCompleted: (result) { + context.go(FlutterShoppingPathRoutes.orderSuccess); + }, + ), ), ), GoRoute( 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 index 41bda86..0926a0f 100644 --- 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 @@ -35,10 +35,10 @@ Future onCompleteOrderDetails( /// /// You can create your own implementation if you decide to use a different /// approach. -void onCompleteShoppingCart( +Future onCompleteShoppingCart( BuildContext context, -) { - context.go(FlutterShoppingPathRoutes.orderDetails); +) async { + await context.push(FlutterShoppingPathRoutes.orderDetails); } /// Default on complete product page function. diff --git a/packages/flutter_shopping/lib/src/widgets/default_order_succes_widget.dart b/packages/flutter_shopping/lib/src/widgets/default_order_succes_widget.dart index 645959a..b754054 100644 --- a/packages/flutter_shopping/lib/src/widgets/default_order_succes_widget.dart +++ b/packages/flutter_shopping/lib/src/widgets/default_order_succes_widget.dart @@ -16,51 +16,169 @@ class DefaultOrderSucces extends StatelessWidget { 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("Finish Order".toUpperCase()), - ), - ); - - var content = Column( - children: [ - const Spacer(), - Text("#123456", style: theme.textTheme.titleLarge), - const SizedBox(height: 16), - Text( - "Order Succesfully Placed!", - style: theme.textTheme.titleLarge, - ), - Text( - "Thank you for your order!", - style: theme.textTheme.bodyMedium, - ), - const SizedBox(height: 16), - Text( - "Your order will be delivered soon.", - style: theme.textTheme.bodyMedium, - ), - const SizedBox(height: 16), - Text( - "Do you want to order again?", - style: theme.textTheme.bodyMedium, - ), - const Spacer(), - finishOrderButton, - ], - ); - return Scaffold( + appBar: AppBar( + title: Text( + "Confirmation", + style: theme.textTheme.headlineLarge, + ), + ), body: SafeArea( - child: Center( - child: content, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + top: 32, + right: 32, + ), + child: Column( + children: [ + Text( + "Success!", + style: theme.textTheme.titleMedium, + ), + const SizedBox( + height: 4, + ), + Text( + "Thank you Peter for your order!", + style: theme.textTheme.bodyMedium, + ), + const SizedBox( + height: 16, + ), + Text( + "The order was placed at Bakkerij de Goudkorst." + " You can pick this" + " up on Monday, February 7 at 1:00 PM.", + style: theme.textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + const SizedBox( + height: 16, + ), + Text( + "If you want, you can place another order in this street.", + style: theme.textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + const SizedBox( + height: 32, + ), + Text( + "Weekly offers", + style: theme.textTheme.headlineSmall + ?.copyWith(color: Colors.black), + ), + const SizedBox( + height: 4, + ), + ], + ), + ), + SizedBox( + height: 272, + child: ListView( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + children: [ + const SizedBox(width: 32), + _discount(context), + const SizedBox(width: 8), + _discount(context), + const SizedBox(width: 32), + ], + ), + ), + const Spacer(), + FilledButton( + onPressed: () { + configuration.onCompleteUserStory.call(context); + }, + child: Text( + "Place another order", + style: theme.textTheme.displayLarge, + ), + ), + ], ), ), ); } } + +Widget _discount(BuildContext context) { + var theme = Theme.of(context); + return Container( + decoration: BoxDecoration( + border: Border.all( + color: Colors.black, + ), + borderRadius: BorderRadius.circular(10), + ), + width: MediaQuery.of(context).size.width - 64, + height: 200, + child: Stack( + children: [ + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10), + bottomLeft: Radius.circular(10), + bottomRight: Radius.circular(10), + ), + child: Image.network( + "https://picsum.photos/150", + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + ), + ), + Container( + alignment: Alignment.centerLeft, + height: 38, + width: double.infinity, + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10), + ), + ), + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + "Butcher Puurvlees", + style: theme.textTheme.headlineSmall?.copyWith( + color: Colors.white, + ), + ), + ), + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(10), + bottomRight: Radius.circular(10), + ), + ), + alignment: Alignment.centerLeft, + width: MediaQuery.of(context).size.width, + height: 68, + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + "Chicken legs, now for 4,99", + style: theme.textTheme.bodyMedium, + ), + ), + ), + ), + ], + ), + ); +} 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 1860ce3..d2a47a0 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 @@ -136,7 +136,10 @@ Widget _defaultProductItemBuilder( await showModalBottomSheet( context: context, backgroundColor: theme.colorScheme.surface, - builder: (context) => ProductItemPopup(product: product, configuration: configuration) + builder: (context) => ProductItemPopup( + product: product, + configuration: configuration, + ), ); }, icon: Icon(