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..ab0d019 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", + ]; + + 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", + ), + ], + ), + ), + ), + ]; } 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_input.dart b/packages/flutter_order_details/lib/src/models/order_input.dart deleted file mode 100644 index d3ac019..0000000 --- a/packages/flutter_order_details/lib/src/models/order_input.dart +++ /dev/null @@ -1,173 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_order_details/src/configuration/order_detail_title_style.dart"; - -/// Abstract class for order detail input. -/// Each input field must extend from this class. -abstract class OrderDetailInput { - /// Constructor for the order detail input. - OrderDetailInput({ - required this.title, - required this.outputKey, - this.titleStyle = OrderDetailTitleStyle.text, - this.titleAlignment = Alignment.centerLeft, - this.titlePadding = const EdgeInsets.symmetric(vertical: 4), - this.subtitle, - this.isRequired = true, - this.isReadOnly = false, - this.initialValue, - this.validators = const [], - this.onValueChanged, - this.hint, - this.errorIsRequired = "This field is required", - this.paddingBetweenFields = const EdgeInsets.symmetric(vertical: 4), - }); - - /// Title of the input field. - final String title; - - /// Subtitle of the input field. - final String? subtitle; - - /// The styling for the title. - final OrderDetailTitleStyle titleStyle; - - /// The alignment of the titl - final Alignment titleAlignment; - - /// Padding around the title. - final EdgeInsets titlePadding; - - /// The output key of the input field. - final String outputKey; - - /// Hint message of the input field. - final String? hint; - - /// Determines if the input field is required. - final bool isRequired; - - /// Error message for when an user does not insert something in the field - /// even though it is required. - final String errorIsRequired; - - /// A read-only field that users cannot change. - final bool isReadOnly; - - /// An initial value for the input field. This is ideal incombination - /// with the [isReadOnly] field. - final T? initialValue; - - /// Internal current value. Do not use. - T? currentValue; - - /// List of validators that should be executed when the input field - /// is validated. - List validators; - - /// Function that is called when the value of the input field changes. - final Function(T)? onValueChanged; - - /// Padding between the fields. - final EdgeInsets paddingBetweenFields; - - /// Allows you to update the current value. - @protected - set updateValue(T value) { - currentValue = value; - } - - /// Function that validates the input field. Automatically keeps track - /// of the [isRequired] keys and all the custom validators. - @protected - String? validate(T? value) { - if (isRequired && (value == null || value.toString().isEmpty)) { - return errorIsRequired; - } - - for (var validator in validators) { - var error = validator(value); - if (error != null) { - return error; - } - } - - return null; - } - - /// Builds the basic outline of an input field. - @protected - Widget buildOutline( - BuildContext context, - // ignore: avoid_annotating_with_dynamic - dynamic child, - Function({bool needsBlur}) onBlurBackground, - ) { - var theme = Theme.of(context); - - return Column( - children: [ - if (titleStyle == OrderDetailTitleStyle.text) ...[ - Align( - alignment: titleAlignment, - child: Padding( - padding: titlePadding, - child: Text( - title, - style: theme.textTheme.titleMedium, - ), - ), - ), - if (subtitle != null) ...[ - Padding( - padding: titlePadding, - child: Align( - alignment: titleAlignment, - child: Text( - subtitle!, - style: theme.textTheme.titleSmall, - ), - ), - ), - ], - ], - if (child is FormField || child is Widget) ...[ - child, - ] else if (child is List) ...[ - Column( - children: child - .map( - (FormField field) => Padding( - padding: paddingBetweenFields, - child: field, - ), - ) - .toList(), - ), - ] else if (child is List) ...[ - Column( - children: child - .map( - (OrderDetailInput input) => Padding( - padding: paddingBetweenFields, - child: input.build( - context, - input.initialValue, - onBlurBackground, - ), - ), - ) - .toList(), - ), - ], - ], - ); - } - - /// Abstract build function that each orderinput class must implement - /// themsleves. For a basic layout, they can use the [buildOutline] function. - Widget build( - BuildContext context, - T? buildInitialValue, - Function({bool needsBlur}) 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_order_details/pubspec.yaml b/packages/flutter_order_details/pubspec.yaml index 7d37a9f..20721cb 100644 --- a/packages/flutter_order_details/pubspec.yaml +++ b/packages/flutter_order_details/pubspec.yaml @@ -1,6 +1,7 @@ name: flutter_order_details description: "A Flutter module for order details." -version: 1.0.0 +version: 2.0.0 +publish_to: 'none' environment: sdk: '>=3.3.0 <4.0.0' @@ -8,6 +9,14 @@ environment: dependencies: flutter: sdk: flutter + animated_toggle: + git: + url: https://github.com/Iconica-Development/flutter_animated_toggle + ref: 0.0.3 + flutter_form_wizard: + git: + url: https://github.com/Iconica-Development/flutter_form_wizard + ref: 6.5.0 dev_dependencies: flutter_test: @@ -18,13 +27,4 @@ dev_dependencies: ref: 7.0.0 flutter: - # 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_product_page/lib/flutter_product_page.dart b/packages/flutter_product_page/lib/flutter_product_page.dart index 2814df5..dbcd53a 100644 --- a/packages/flutter_product_page/lib/flutter_product_page.dart +++ b/packages/flutter_product_page/lib/flutter_product_page.dart @@ -7,7 +7,6 @@ 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.dart"; export "src/models/product_page_shop.dart"; export "src/ui/product_page.dart"; export "src/ui/product_page_screen.dart"; 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 a08c3d0..d3f440c 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,47 +1,38 @@ import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.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"; /// Configuration for the product page. class ProductPageConfiguration { /// Constructor for the product page configuration. ProductPageConfiguration({ required this.shops, - // required this.getProducts, - // required this.onAddToCart, required this.onNavigateToShoppingCart, - this.navigateToShoppingCartBuilder, - // + this.navigateToShoppingCartBuilder = _defaultNavigateToShoppingCartBuilder, this.initialShopId, - // this.productBuilder, - // this.onShopSelectionChange, this.getProductsInShoppingCart, - // this.localizations = const ProductPageLocalization(), - // this.shopSelectorStyle = ShopSelectorStyle.spacedWrap, this.categoryStylingConfiguration = const ProductPageCategoryStylingConfiguration(), - // this.pagePadding = const EdgeInsets.all(4), - // - this.appBar, + this.appBar = _defaultAppBar, this.bottomNavigationBar, - // Function( BuildContext context, - ProductPageProduct product, + Product product, )? onProductDetail, String Function( - ProductPageProduct product, + Product product, )? getDiscountDescription, Widget Function( BuildContext context, - ProductPageProduct product, + Product product, )? productPopupBuilder, Widget Function( BuildContext context, @@ -54,14 +45,13 @@ class ProductPageConfiguration { }) { _productPopupBuilder = productPopupBuilder; _productPopupBuilder ??= - (BuildContext context, ProductPageProduct product) => ProductItemPopup( + (BuildContext context, Product product) => ProductItemPopup( product: product, configuration: this, ); _onProductDetail = onProductDetail; - _onProductDetail ??= - (BuildContext context, ProductPageProduct product) async { + _onProductDetail ??= (BuildContext context, Product product) async { var theme = Theme.of(context); await showModalBottomSheet( @@ -98,8 +88,8 @@ class ProductPageConfiguration { }; _getDiscountDescription = getDiscountDescription; - _getDiscountDescription ??= - (ProductPageProduct product) => "${product.name} is on sale!"; + _getDiscountDescription ??= (Product product) => + "${product.name}, now for ${product.discountPrice} each"; } /// The shop that is initially selected. @@ -119,27 +109,25 @@ class ProductPageConfiguration { /// 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, ProductPageProduct product)? - productBuilder; + Widget Function(BuildContext context, Product product)? productBuilder; - late Widget Function(BuildContext context, ProductPageProduct product)? + 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, ProductPageProduct product) + Widget Function(BuildContext context, Product product) get productPopupBuilder => _productPopupBuilder!; - late Function(BuildContext context, ProductPageProduct product)? - _onProductDetail; + 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, ProductPageProduct product) - get onProductDetail => _onProductDetail!; + Function(BuildContext context, Product product) get onProductDetail => + _onProductDetail!; late Widget Function(BuildContext context)? _noContentBuilder; @@ -149,7 +137,11 @@ class ProductPageConfiguration { /// The builder for the shopping cart. This builder should return a widget /// that navigates to the shopping cart overview page. - Widget Function(BuildContext context)? navigateToShoppingCartBuilder; + Widget Function( + BuildContext context, + ProductPageConfiguration configuration, + ShoppingCartNotifier notifier, + ) navigateToShoppingCartBuilder; late Widget Function( BuildContext context, @@ -162,16 +154,16 @@ class ProductPageConfiguration { Widget Function(BuildContext context, Object? error, StackTrace? stackTrace)? get errorBuilder => _errorBuilder; - late String Function(ProductPageProduct product)? _getDiscountDescription; + 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(ProductPageProduct product)? get getDiscountDescription => + String Function(Product product)? get getDiscountDescription => _getDiscountDescription!; /// This function must be implemented by the developer and should handle the /// adding of a product to the cart. - Function(ProductPageProduct product) onAddToCart; + Function(Product product) onAddToCart; /// This function gets executed when the user changes the shop selection. /// This function always fires upon first load with the initial shop as well. @@ -198,5 +190,60 @@ class ProductPageConfiguration { final Widget? bottomNavigationBar; /// Optional app bar that you can pass to the order detail screen. - final PreferredSizeWidget? appBar; + final AppBar Function(BuildContext context)? appBar; +} + +AppBar _defaultAppBar( + BuildContext context, +) { + 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, + ), + ); +} + +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, + ), + ), + ), + ), + ), + ); } 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 index ec172a4..018d822 100644 --- a/packages/flutter_product_page/lib/src/configuration/product_page_content.dart +++ b/packages/flutter_product_page/lib/src/configuration/product_page_content.dart @@ -1,4 +1,4 @@ -import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; /// Return type that contains the products and an optional discounted product. class ProductPageContent { @@ -9,8 +9,8 @@ class ProductPageContent { }); /// List of products that belong to the shop. - final List products; + final List products; /// Optional highlighted discounted product to display. - final ProductPageProduct? discountedProduct; + 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_localization.dart index 47990f6..f2bbe3f 100644 --- a/packages/flutter_product_page/lib/src/configuration/product_page_localization.dart +++ b/packages/flutter_product_page/lib/src/configuration/product_page_localization.dart @@ -2,8 +2,8 @@ class ProductPageLocalization { /// Default constructor const ProductPageLocalization({ - this.navigateToShoppingCart = "To shopping cart", - this.discountTitle = "Discount", + this.navigateToShoppingCart = "View shopping cart", + this.discountTitle = "Weekly offer", this.failedToLoadImageExplenation = "Failed to load image", this.close = "Close", }); diff --git a/packages/flutter_product_page/lib/src/models/product.dart b/packages/flutter_product_page/lib/src/models/product.dart deleted file mode 100644 index 4e09fdb..0000000 --- a/packages/flutter_product_page/lib/src/models/product.dart +++ /dev/null @@ -1,26 +0,0 @@ -/// The product page shop class contains all the required information -/// -/// This is a mixin class because another package will implement it, and the -/// 'MyProduct' class might have to extend another class as well. -mixin ProductPageProduct { - /// The unique identifier for the product. - String get id; - - /// The name of the product. - String get name; - - /// The image URL of the product. - String get imageUrl; - - /// The category of the product. - String get category; - - /// The price of the product. - double get price; - - /// Whether the product has a discount or not. - bool get hasDiscount; - - /// The discounted price of the product. Only used if [hasDiscount] is true. - double? get discountPrice; -} 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 76541be..5d39843 100644 --- a/packages/flutter_product_page/lib/src/services/category_service.dart +++ b/packages/flutter_product_page/lib/src/services/category_service.dart @@ -1,14 +1,14 @@ import "package:flutter/material.dart"; import "package:flutter_nested_categories/flutter_nested_categories.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; import "package:flutter_product_page/src/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. -ProductPageProduct onAddToCartWrapper( +Product onAddToCartWrapper( ProductPageConfiguration configuration, ShoppingCartNotifier shoppingCartNotifier, - ProductPageProduct product, + Product product, ) { shoppingCartNotifier.productsChanged(); @@ -19,13 +19,14 @@ ProductPageProduct onAddToCartWrapper( /// Generates a [CategoryList] from a list of [Product]s and a /// [ProductPageConfiguration]. -CategoryList getCategoryList( +Widget getCategoryList( BuildContext context, ProductPageConfiguration configuration, ShoppingCartNotifier shoppingCartNotifier, - List products, + List products, ) { - var categorizedProducts = >{}; + var theme = Theme.of(context); + var categorizedProducts = >{}; for (var product in products) { if (!categorizedProducts.containsKey(product.category)) { categorizedProducts[product.category] = []; @@ -43,8 +44,7 @@ CategoryList getCategoryList( : ProductItem( product: product, onProductDetail: configuration.onProductDetail, - onAddToCart: (ProductPageProduct product) => - onAddToCartWrapper( + onAddToCart: (Product product) => onAddToCartWrapper( configuration, shoppingCartNotifier, product, @@ -59,15 +59,19 @@ CategoryList getCategoryList( ); categories.add(category); }); - - return CategoryList( - title: configuration.categoryStylingConfiguration.title, - titleStyle: configuration.categoryStylingConfiguration.titleStyle, - customTitle: configuration.categoryStylingConfiguration.customTitle, - headerCentered: configuration.categoryStylingConfiguration.headerCentered, - headerStyling: configuration.categoryStylingConfiguration.headerStyling, - isCategoryCollapsible: - configuration.categoryStylingConfiguration.isCategoryCollapsible, - content: categories, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var category in categories) ...[ + Text( + category.name!, + style: theme.textTheme.titleMedium, + ), + Column( + children: category.content, + ), + const SizedBox(height: 16), + ], + ], ); } diff --git a/packages/flutter_product_page/lib/src/ui/components/product_item.dart b/packages/flutter_product_page/lib/src/ui/components/product_item.dart index 05ed65d..ffcea2c 100644 --- a/packages/flutter_product_page/lib/src/ui/components/product_item.dart +++ b/packages/flutter_product_page/lib/src/ui/components/product_item.dart @@ -1,6 +1,6 @@ import "package:cached_network_image/cached_network_image.dart"; import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; import "package:skeletonizer/skeletonizer.dart"; /// Product item widget. @@ -15,14 +15,13 @@ class ProductItem extends StatelessWidget { }); /// Product to display. - final ProductPageProduct product; + final Product product; /// Function to call when the product detail is requested. - final Function(BuildContext context, ProductPageProduct selectedProduct) - onProductDetail; + final Function(BuildContext context, Product selectedProduct) onProductDetail; /// Function to call when the product is added to the cart. - final Function(ProductPageProduct selectedProduct) onAddToCart; + final Function(Product selectedProduct) onAddToCart; /// Localizations for the product page. final ProductPageLocalization localizations; @@ -76,7 +75,10 @@ class ProductItem extends StatelessWidget { padding: const EdgeInsets.only(left: 4), child: IconButton( onPressed: () => onProductDetail(context, product), - icon: const Icon(Icons.info_outline), + icon: Icon( + Icons.info_outline, + color: theme.colorScheme.primary, + ), ), ); @@ -84,10 +86,7 @@ class ProductItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, children: [ _PriceLabel( - price: product.price, - discountPrice: (product.hasDiscount && product.discountPrice != null) - ? product.discountPrice - : null, + product: product, ), _AddToCardButton( product: product, @@ -113,42 +112,36 @@ class ProductItem extends StatelessWidget { class _PriceLabel extends StatelessWidget { const _PriceLabel({ - required this.price, - required this.discountPrice, + required this.product, }); - final double price; - final double? discountPrice; + final Product product; @override Widget build(BuildContext context) { var theme = Theme.of(context); - if (discountPrice == null) - return Text( - price.toStringAsFixed(2), - style: theme.textTheme.bodyMedium, - ); - else - return Row( - children: [ + return Row( + children: [ + if (product.hasDiscount) ...[ Text( - price.toStringAsFixed(2), + product.price.toStringAsFixed(2), style: theme.textTheme.bodySmall?.copyWith( - fontSize: 10, - color: theme.colorScheme.primary, decoration: TextDecoration.lineThrough, ), + textAlign: TextAlign.center, ), - Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text( - discountPrice!.toStringAsFixed(2), - style: theme.textTheme.bodyMedium, - ), - ), + const SizedBox(width: 4), ], - ); + Text( + product.hasDiscount + ? product.discountPrice!.toStringAsFixed(2) + : product.price.toStringAsFixed(2), + style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ], + ); } } @@ -158,36 +151,30 @@ class _AddToCardButton extends StatelessWidget { required this.onAddToCart, }); - final ProductPageProduct product; - final Function(ProductPageProduct product) onAddToCart; + final Product product; + final Function(Product product) onAddToCart; static const double boxSize = 29; @override Widget build(BuildContext context) { var theme = Theme.of(context); - return SizedBox( + return Container( + decoration: BoxDecoration( + color: theme.colorScheme.primary, + borderRadius: BorderRadius.circular(4), + ), width: boxSize, height: boxSize, child: Center( child: IconButton( padding: EdgeInsets.zero, - icon: Icon( + icon: const Icon( Icons.add, - color: theme.primaryColor, + color: Colors.white, size: 20, ), onPressed: () => onAddToCart(product), - style: ButtonStyle( - backgroundColor: WidgetStateProperty.all( - theme.colorScheme.secondary, - ), - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - ), - ), ), ), ); diff --git a/packages/flutter_product_page/lib/src/ui/components/weekly_discount.dart b/packages/flutter_product_page/lib/src/ui/components/weekly_discount.dart index 80da812..e82a270 100644 --- a/packages/flutter_product_page/lib/src/ui/components/weekly_discount.dart +++ b/packages/flutter_product_page/lib/src/ui/components/weekly_discount.dart @@ -1,6 +1,6 @@ import "package:cached_network_image/cached_network_image.dart"; import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; /// A widget that displays a weekly discount. class WeeklyDiscount extends StatelessWidget { @@ -15,10 +15,10 @@ class WeeklyDiscount extends StatelessWidget { final ProductPageConfiguration configuration; /// The product for which the discount is displayed. - final ProductPageProduct product; + final Product product; /// The top padding of the widget. - static const double topPadding = 32.0; + static const double topPadding = 20; @override Widget build(BuildContext context) { @@ -28,9 +28,7 @@ class WeeklyDiscount extends StatelessWidget { padding: const EdgeInsets.all(20.0), child: Text( configuration.getDiscountDescription!(product), - style: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.primary, - ), + style: theme.textTheme.bodyMedium, textAlign: TextAlign.left, ), ); @@ -73,9 +71,9 @@ class WeeklyDiscount extends StatelessWidget { ); var topText = DecoratedBox( - decoration: BoxDecoration( - color: theme.primaryColor, - borderRadius: const BorderRadius.only( + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.only( topLeft: Radius.circular(4), topRight: Radius.circular(4), ), @@ -88,10 +86,8 @@ class WeeklyDiscount extends StatelessWidget { horizontal: 16, ), child: Text( - configuration.localizations.discountTitle.toUpperCase(), - style: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.onPrimary, - ), + configuration.localizations.discountTitle, + style: theme.textTheme.headlineSmall, textAlign: TextAlign.left, ), ), @@ -100,7 +96,6 @@ class WeeklyDiscount extends StatelessWidget { var boxDecoration = BoxDecoration( border: Border.all( - color: theme.primaryColor, width: 1.0, ), borderRadius: BorderRadius.circular(4.0), diff --git a/packages/flutter_product_page/lib/src/ui/product_page.dart b/packages/flutter_product_page/lib/src/ui/product_page.dart index 5c99990..bbdfe8f 100644 --- a/packages/flutter_product_page/lib/src/ui/product_page.dart +++ b/packages/flutter_product_page/lib/src/ui/product_page.dart @@ -121,6 +121,7 @@ class _ProductPage extends StatelessWidget { Widget build(BuildContext context) { var pageContent = SingleChildScrollView( child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ ShopSelector( configuration: configuration, @@ -142,63 +143,13 @@ class _ProductPage extends StatelessWidget { pageContent, Align( alignment: Alignment.bottomCenter, - child: configuration.navigateToShoppingCartBuilder != null - ? configuration.navigateToShoppingCartBuilder!(context) - : _NavigateToShoppingCartButton( - configuration: configuration, - shoppingCartNotifier: shoppingCartNotifier, - ), - ), - ], - ); - } -} - -class _NavigateToShoppingCartButton extends StatelessWidget { - const _NavigateToShoppingCartButton({ - required this.configuration, - required this.shoppingCartNotifier, - }); - - final ProductPageConfiguration configuration; - final ShoppingCartNotifier shoppingCartNotifier; - - @override - Widget build(BuildContext context) { - var theme = Theme.of(context); - - String getProductsInShoppingCartLabel() { - var fun = configuration.getProductsInShoppingCart; - - if (fun == null) { - return ""; - } - - return "(${fun()})"; - } - - return FilledButton( - onPressed: configuration.onNavigateToShoppingCart, - style: theme.filledButtonTheme.style?.copyWith( - backgroundColor: WidgetStateProperty.all( - theme.colorScheme.primary, - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - child: ListenableBuilder( - listenable: shoppingCartNotifier, - builder: (BuildContext context, Widget? _) => Text( - """${configuration.localizations.navigateToShoppingCart.toUpperCase()} ${getProductsInShoppingCartLabel()}""", - style: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.onPrimary, - ), + child: configuration.navigateToShoppingCartBuilder( + context, + configuration, + shoppingCartNotifier, ), ), - ), + ], ); } } @@ -215,74 +166,87 @@ class _ShopContents extends StatelessWidget { final ShoppingCartNotifier shoppingCartNotifier; @override - Widget build(BuildContext context) => Padding( - padding: EdgeInsets.symmetric( - horizontal: configuration.pagePadding.horizontal, + 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!, ), - 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, 24, 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), - ], - ), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Align( + alignment: Alignment.center, + child: CircularProgressIndicator.adaptive(), ); + } - return Column( + 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: [ - // Discounted product - if (productPageContent.discountedProduct != null) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: WeeklyDiscount( - configuration: configuration, - product: productPageContent.discountedProduct!, - ), - ), - ], + // Products + getCategoryList( + context, + configuration, + shoppingCartNotifier, + productPageContent.products, + ), - productList, + // 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 index 959626b..ceaa158 100644 --- a/packages/flutter_product_page/lib/src/ui/product_page_screen.dart +++ b/packages/flutter_product_page/lib/src/ui/product_page_screen.dart @@ -24,13 +24,13 @@ class ProductPageScreen extends StatelessWidget { @override Widget build(BuildContext context) => Scaffold( + appBar: configuration.appBar!.call(context), body: SafeArea( child: ProductPage( configuration: configuration, initialBuildShopId: initialBuildShopId, ), ), - appBar: configuration.appBar, bottomNavigationBar: configuration.bottomNavigationBar, ); } diff --git a/packages/flutter_product_page/lib/src/ui/widgets/horizontal_list_items.dart b/packages/flutter_product_page/lib/src/ui/widgets/horizontal_list_items.dart index 8079723..515f17b 100644 --- a/packages/flutter_product_page/lib/src/ui/widgets/horizontal_list_items.dart +++ b/packages/flutter_product_page/lib/src/ui/widgets/horizontal_list_items.dart @@ -9,7 +9,7 @@ class HorizontalListItems extends StatelessWidget { required this.selectedItem, required this.onTap, this.paddingBetweenButtons = 2.0, - this.paddingOnButtons = 4, + this.paddingOnButtons = 6, super.key, }); @@ -32,41 +32,46 @@ class HorizontalListItems extends StatelessWidget { Widget build(BuildContext context) { var theme = Theme.of(context); - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: shops - .map( - (shop) => Padding( - padding: EdgeInsets.only(right: paddingBetweenButtons), - child: InkWell( - onTap: () => onTap(shop), - child: Container( - decoration: BoxDecoration( - color: shop.id == selectedItem - ? theme.colorScheme.primary - : theme.colorScheme.secondary, - borderRadius: BorderRadius.circular(4), - border: Border.all( - color: theme.colorScheme.primary, - width: 1, + return Padding( + padding: const EdgeInsets.only( + top: 4, + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: shops + .map( + (shop) => Padding( + padding: EdgeInsets.only(right: paddingBetweenButtons), + child: InkWell( + onTap: () => onTap(shop), + child: Container( + decoration: BoxDecoration( + color: shop.id == selectedItem + ? theme.colorScheme.primary + : Colors.white, + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: theme.colorScheme.primary, + width: 1, + ), + ), + padding: EdgeInsets.all(paddingOnButtons), + child: Text( + shop.name, + style: shop.id == selectedItem + ? theme.textTheme.bodyMedium?.copyWith( + color: Colors.white, + fontWeight: FontWeight.bold, + ) + : theme.textTheme.bodyMedium, ), - ), - padding: EdgeInsets.all(paddingOnButtons), - child: Text( - shop.name, - style: shop.id == selectedItem - ? theme.textTheme.bodyMedium?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - ) - : theme.textTheme.bodyMedium, ), ), ), - ), - ) - .toList(), + ) + .toList(), + ), ), ); } diff --git a/packages/flutter_product_page/lib/src/ui/widgets/product_item_popup.dart b/packages/flutter_product_page/lib/src/ui/widgets/product_item_popup.dart index 5186ef5..099fe49 100644 --- a/packages/flutter_product_page/lib/src/ui/widgets/product_item_popup.dart +++ b/packages/flutter_product_page/lib/src/ui/widgets/product_item_popup.dart @@ -1,5 +1,5 @@ import "package:flutter/material.dart"; -import "package:flutter_product_page/flutter_product_page.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; /// A popup that displays the product item. class ProductItemPopup extends StatelessWidget { @@ -11,7 +11,7 @@ class ProductItemPopup extends StatelessWidget { }); /// The product to display. - final ProductPageProduct product; + final Product product; /// Configuration for the product page. final ProductPageConfiguration configuration; @@ -20,48 +20,44 @@ class ProductItemPopup extends StatelessWidget { Widget build(BuildContext context) { var theme = Theme.of(context); - var productDescription = Padding( - padding: const EdgeInsets.fromLTRB(44, 32, 44, 20), - child: Text( - product.name, - textAlign: TextAlign.center, - ), - ); - - var closeButton = Padding( - padding: const EdgeInsets.fromLTRB(80, 0, 80, 32), - child: SizedBox( - width: 254, - child: ElevatedButton( - style: theme.elevatedButtonTheme.style?.copyWith( - shape: WidgetStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(6), - ), - ), - ), - onPressed: () => Navigator.of(context).pop(), - child: Padding( - padding: const EdgeInsets.all(14), - child: Text( - configuration.localizations.close, - style: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.onSurface, - ), - ), - ), - ), - ), - ); - return SingleChildScrollView( - child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - productDescription, - closeButton, - ], + child: Padding( + padding: const EdgeInsets.all(32), + child: SizedBox( + width: double.infinity, + child: Column( + children: [ + Text( + product.description, + style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, + ), + Padding( + padding: const EdgeInsets.only(top: 20, left: 40, right: 40), + child: SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () => Navigator.of(context).pop(), + style: theme.filledButtonTheme.style?.copyWith( + backgroundColor: WidgetStateProperty.all( + theme.colorScheme.primary, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Text( + configuration.localizations.close, + style: theme.textTheme.displayLarge, + ), + ), + ), + ), + ), + ], + ), ), ), ); diff --git a/packages/flutter_product_page/lib/src/ui/widgets/spaced_wrap.dart b/packages/flutter_product_page/lib/src/ui/widgets/spaced_wrap.dart index 9bfa8b4..66cf0c8 100644 --- a/packages/flutter_product_page/lib/src/ui/widgets/spaced_wrap.dart +++ b/packages/flutter_product_page/lib/src/ui/widgets/spaced_wrap.dart @@ -33,118 +33,44 @@ class SpacedWrap extends StatelessWidget { /// Callback when an item is tapped. final Function(ProductPageShop shop) onTap; - Row _buildRow( - BuildContext context, - List currentRow, - double availableRowLength, - ) { + @override + Widget build(BuildContext context) { var theme = Theme.of(context); - - var row = []; - var extraButtonPadding = availableRowLength / currentRow.length / 2; - - for (var i = 0, len = currentRow.length; i < len; i++) { - var shop = shops[currentRow[i]]; - row.add( - Padding( - padding: EdgeInsets.only(top: paddingBetweenButtons), - child: InkWell( - onTap: () => onTap(shop), - child: Container( - decoration: BoxDecoration( - color: shop.id == selectedItem - ? theme.colorScheme.primary - : theme.colorScheme.secondary, - borderRadius: BorderRadius.circular(4), - border: Border.all( - color: theme.colorScheme.primary, - width: 1, + return Wrap( + alignment: WrapAlignment.center, + spacing: 4, + children: [ + for (var shop in shops) ...[ + Padding( + padding: EdgeInsets.only(top: paddingBetweenButtons), + child: InkWell( + onTap: () => onTap(shop), + child: DecoratedBox( + decoration: BoxDecoration( + color: shop.id == selectedItem + ? Theme.of(context).colorScheme.primary + : Colors.white, + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: 1, + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + shop.name, + style: shop.id == selectedItem + ? theme.textTheme.titleMedium + ?.copyWith(color: Colors.white) + : theme.textTheme.bodyMedium, + ), ), - ), - padding: EdgeInsets.symmetric( - horizontal: paddingOnButtons + extraButtonPadding, - vertical: paddingOnButtons, - ), - child: Text( - shop.name, - style: shop.id == selectedItem - ? theme.textTheme.bodyMedium?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - ) - : theme.textTheme.bodyMedium, ), ), ), - ), - ); - if (shops.last != shop) { - row.add(const Spacer()); - } - } - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: row, + ], + ], ); } - - List _buildButtonRows(BuildContext context) { - var theme = Theme.of(context); - var rows = []; - var currentRow = []; - var availableRowLength = width; - - for (var i = 0; i < shops.length; i++) { - var shop = shops[i]; - - var textPainter = TextPainter( - text: TextSpan( - text: shop.name, - style: shop.id == selectedItem - ? theme.textTheme.bodyMedium?.copyWith( - color: Colors.white, - fontWeight: FontWeight.bold, - ) - : theme.textTheme.bodyMedium, - ), - maxLines: 1, - textDirection: TextDirection.ltr, - )..layout(minWidth: 0, maxWidth: double.infinity); - - var buttonWidth = textPainter.width + paddingOnButtons * 2; - - if (availableRowLength - buttonWidth < 0) { - rows.add( - _buildRow( - context, - currentRow, - availableRowLength, - ), - ); - currentRow = []; - availableRowLength = width; - } - - currentRow.add(i); - - availableRowLength -= buttonWidth + paddingBetweenButtons; - } - if (currentRow.isNotEmpty) { - rows.add( - _buildRow( - context, - currentRow, - availableRowLength, - ), - ); - } - return rows; - } - - @override - Widget build(BuildContext context) => Column( - children: _buildButtonRows( - context, - ), - ); } diff --git a/packages/flutter_product_page/pubspec.yaml b/packages/flutter_product_page/pubspec.yaml index 6ffcb50..2207ff0 100644 --- a/packages/flutter_product_page/pubspec.yaml +++ b/packages/flutter_product_page/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_product_page description: "A Flutter module for the product page" publish_to: 'none' -version: 1.0.0 +version: 2.0.0 environment: sdk: '>=3.3.4 <4.0.0' @@ -15,6 +15,11 @@ dependencies: 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: @@ -26,13 +31,3 @@ dev_dependencies: 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/lib/src/configuration/configuration.dart b/packages/flutter_shopping/example/lib/src/configuration/configuration.dart index 9184c8b..bb3454e 100644 --- a/packages/flutter_shopping/example/lib/src/configuration/configuration.dart +++ b/packages/flutter_shopping/example/lib/src/configuration/configuration.dart @@ -1,4 +1,3 @@ -import "package:example/src/models/my_product.dart"; import "package:example/src/routes.dart"; import "package:example/src/services/order_service.dart"; import "package:example/src/services/shop_service.dart"; @@ -7,7 +6,7 @@ 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([]); +final ProductService productService = ProductService([]); FlutterShoppingConfiguration getFlutterShoppingConfiguration() => FlutterShoppingConfiguration( @@ -24,8 +23,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() => shops: Future.value(getShops()), // (REQUIRED): Function to add a product to the cart - onAddToCart: (ProductPageProduct product) => - productService.addProduct(product as MyProduct), + onAddToCart: productService.addProduct, // (REQUIRED): Function to get the products for a shop getProducts: (ProductPageShop shop) => @@ -34,7 +32,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 @@ -43,7 +41,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() => // (RECOMMENDED) Function that returns the description for a // product that is on sale. - getDiscountDescription: (ProductPageProduct product) => + getDiscountDescription: (product) => """${product.name} for just \$${product.discountPrice?.toStringAsFixed(2)}""", // (RECOMMENDED) Function that is fired when the shop selection @@ -60,7 +58,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() => localizations: const ProductPageLocalization(), // (OPTIONAL) Appbar - appBar: AppBar( + appBar: (context) => AppBar( title: const Text("Shop"), leading: IconButton( icon: const Icon( @@ -85,7 +83,8 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() => productService: productService, // (REQUIRED) product item builder: - productItemBuilder: (context, locale, product) => ListTile( + productItemBuilder: (context, locale, product, service, config) => + ListTile( title: Text(product.name), subtitle: Text(product.price.toStringAsFixed(2)), leading: Image.network( @@ -116,14 +115,11 @@ 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(), - // (OPTIONAL) title above product list: - title: "Products", - /// (OPTIONAL) no content builder for when there are no products /// in the shopping cart. noContentBuilder: (context) => const Center( diff --git a/packages/flutter_shopping/example/lib/src/models/my_product.dart b/packages/flutter_shopping/example/lib/src/models/my_product.dart deleted file mode 100644 index f5f2f49..0000000 --- a/packages/flutter_shopping/example/lib/src/models/my_product.dart +++ /dev/null @@ -1,25 +0,0 @@ -import "package:flutter_shopping/flutter_shopping.dart"; - -class MyProduct extends ShoppingCartProduct with ProductPageProduct { - MyProduct({ - required super.id, - required super.name, - required super.price, - required this.category, - required this.imageUrl, - this.discountPrice, - this.hasDiscount = false, - }); - - @override - final String category; - - @override - final String imageUrl; - - @override - final double? discountPrice; - - @override - final bool hasDiscount; -} diff --git a/packages/flutter_shopping/example/lib/src/services/order_service.dart b/packages/flutter_shopping/example/lib/src/services/order_service.dart index fe3ca31..6078e6a 100644 --- a/packages/flutter_shopping/example/lib/src/services/order_service.dart +++ b/packages/flutter_shopping/example/lib/src/services/order_service.dart @@ -1,7 +1,6 @@ -import "package:example/src/models/my_product.dart"; import "package:flutter_shopping/flutter_shopping.dart"; /// Example implementation of storing an order in a database. -void storeOrderInDatabase(List products, OrderResult result) { +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 index ced2144..a8e8f65 100644 --- a/packages/flutter_shopping/example/lib/src/services/shop_service.dart +++ b/packages/flutter_shopping/example/lib/src/services/shop_service.dart @@ -1,4 +1,3 @@ -import "package:example/src/models/my_product.dart"; import "package:example/src/models/my_shop.dart"; import "package:flutter_shopping/flutter_shopping.dart"; @@ -20,8 +19,8 @@ ProductPageContent getShopContent(String shopId) { /// 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) => [ - MyProduct( +List getProducts(String shopId) => [ + Product( id: "1", name: "White bread", price: 2.99, @@ -29,19 +28,22 @@ List getProducts(String shopId) => [ imageUrl: "https://via.placeholder.com/150", hasDiscount: true, discountPrice: 1.99, + description: "", ), - MyProduct( + Product( id: "2", name: "Brown bread", price: 2.99, category: "Loaves", imageUrl: "https://via.placeholder.com/150", + description: "", ), - MyProduct( + 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/pubspec.yaml b/packages/flutter_shopping/example/pubspec.yaml index d4a84ed..d9bf03b 100644 --- a/packages/flutter_shopping/example/pubspec.yaml +++ b/packages/flutter_shopping/example/pubspec.yaml @@ -12,12 +12,11 @@ dependencies: flutter_hooks: ^0.20.0 hooks_riverpod: ^2.1.1 go_router: 12.1.3 - - # Iconica packages - - ## Userstories flutter_shopping: - path: ../ + git: + url: https://github.com/Iconica-Development/flutter_shopping + path: packages/flutter_shopping + ref: 2.0.0 dev_dependencies: flutter_test: 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 index 1ab239c..3bcce30 100644 --- a/packages/flutter_shopping/example_amazon/lib/src/configuration/shopping_configuration.dart +++ b/packages/flutter_shopping/example_amazon/lib/src/configuration/shopping_configuration.dart @@ -1,4 +1,3 @@ -import "package:amazon/src/models/my_product.dart"; import "package:amazon/src/routes.dart"; import "package:amazon/src/services/category_service.dart"; import "package:flutter/material.dart"; @@ -6,7 +5,7 @@ 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([]); +final ProductService productService = ProductService([]); FlutterShoppingConfiguration getFlutterShoppingConfiguration() => FlutterShoppingConfiguration( @@ -27,8 +26,9 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() => pagePadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), // (REQUIRED): Function to add a product to the cart - onAddToCart: (ProductPageProduct product) => - productService.addProduct(product as MyProduct), + onAddToCart: (product) { + return productService.addProduct(product); + }, // (REQUIRED): Function to get the products for a shop getProducts: (ProductPageShop shop) => @@ -41,7 +41,9 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() => shopSelectorStyle: ShopSelectorStyle.row, - navigateToShoppingCartBuilder: (context) => const SizedBox.shrink(), + navigateToShoppingCartBuilder: (context, productpageinfo, shop) { + return const SizedBox.shrink(); + }, bottomNavigationBar: BottomNavigationBar( fixedColor: theme.primaryColor, @@ -164,7 +166,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() => const SizedBox(height: 12), FilledButton( onPressed: () { - productService.addProduct(product as MyProduct); + productService.addProduct(product); }, child: const Text("In winkelwagen"), ), @@ -206,7 +208,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() => ), // (OPTIONAL) Appbar - appBar: AppBar( + appBar: (context) => AppBar( title: const SizedBox( height: 40, child: SearchBar( @@ -262,7 +264,8 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() => productService: productService, // (REQUIRED) product item builder: - productItemBuilder: (context, locale, product) => ListTile( + productItemBuilder: (context, locale, product, service, config) => + ListTile( title: Text(product.name), subtitle: Text(product.price.toStringAsFixed(2)), leading: Image.network( diff --git a/packages/flutter_shopping/example_amazon/lib/src/models/my_product.dart b/packages/flutter_shopping/example_amazon/lib/src/models/my_product.dart deleted file mode 100644 index 89f8bd1..0000000 --- a/packages/flutter_shopping/example_amazon/lib/src/models/my_product.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter_shopping/flutter_shopping.dart'; - -class MyProduct extends ShoppingCartProduct with ProductPageProduct { - MyProduct({ - required super.id, - required super.name, - required super.price, - required this.category, - required this.imageUrl, - }); - - @override - final String category; - - @override - final String imageUrl; - - @override - final double? discountPrice = 0.0; - - @override - final bool hasDiscount = false; -} 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 index d4aa2f6..69af668 100644 --- a/packages/flutter_shopping/example_amazon/lib/src/services/category_service.dart +++ b/packages/flutter_shopping/example_amazon/lib/src/services/category_service.dart @@ -1,5 +1,4 @@ import "package:amazon/src/models/my_category.dart"; -import "package:amazon/src/models/my_product.dart"; import "package:flutter_shopping/flutter_shopping.dart"; Map categories = { @@ -8,8 +7,8 @@ Map categories = { "TV's": "TV's", }; -List allProducts() => [ - MyProduct( +List allProducts() => [ + Product( id: "1", name: "Skar Audio Single 8\" Complete 1,200 Watt EVL Series Subwoofer Bass Package - Includes Loaded Enclosure with...", @@ -17,8 +16,9 @@ List allProducts() => [ category: categories["Electronics"]!, imageUrl: "https://m.media-amazon.com/images/I/710n3hnbfXL._AC_UY218_.jpg", + description: "", ), - MyProduct( + Product( id: "2", name: "Frameo 10.1 Inch WiFi Digital Picture Frame, 1280x800 HD IPS Touch Screen Photo Frame Electronic, 32GB Memory, Auto...", @@ -26,8 +26,9 @@ List allProducts() => [ category: categories["Electronics"]!, imageUrl: "https://m.media-amazon.com/images/I/61O+aorCp0L._AC_UY218_.jpg", + description: "", ), - MyProduct( + Product( id: "3", name: "STREBITO Electronics Precision Screwdriver Sets 142-Piece with 120 Bits Magnetic Repair Tool Kit for iPhone, MacBook,...", @@ -35,8 +36,9 @@ List allProducts() => [ category: categories["Electronics"]!, imageUrl: "https://m.media-amazon.com/images/I/81-C7lGtQsL._AC_UY218_.jpg", + description: "", ), - MyProduct( + Product( id: "4", name: "Samsung Galaxy A15 (SM-155M/DSN), 128GB 6GB RAM, Dual SIM, Factory Unlocked GSM, International Version (Wall...", @@ -44,8 +46,9 @@ List allProducts() => [ category: categories["Smart phones"]!, imageUrl: "https://m.media-amazon.com/images/I/51rp0nqaPoL._AC_UY218_.jpg", + description: "", ), - MyProduct( + Product( id: "5", name: "SAMSUNG Galaxy S24 Ultra Cell Phone, 512GB AI Smartphone, Unlocked Android, 50MP Zoom Camera, Long...", @@ -53,6 +56,7 @@ List allProducts() => [ category: categories["Smart phones"]!, imageUrl: "https://m.media-amazon.com/images/I/71ZoDT7a2wL._AC_UY218_.jpg", + description: "", ), ]; @@ -72,7 +76,7 @@ ProductPageContent getShopContent(String shopId) { ); } -List getProducts(String categoryId) { +List getProducts(String categoryId) { if (categoryId == "1") { return allProducts(); } else if (categoryId == "2") { diff --git a/packages/flutter_shopping/example_amazon/pubspec.yaml b/packages/flutter_shopping/example_amazon/pubspec.yaml index 7c5ac3c..fb6267b 100644 --- a/packages/flutter_shopping/example_amazon/pubspec.yaml +++ b/packages/flutter_shopping/example_amazon/pubspec.yaml @@ -17,7 +17,10 @@ dependencies: url: https://github.com/Iconica-Development/flutter_nested_categories ref: 0.0.1 flutter_shopping: - path: ../ + git: + url: https://github.com/Iconica-Development/flutter_shopping + path: packages/flutter_shopping + ref: 2.0.0 dev_dependencies: flutter_test: diff --git a/packages/flutter_shopping/lib/flutter_shopping.dart b/packages/flutter_shopping/lib/flutter_shopping.dart index 586414f..5e962e1 100644 --- a/packages/flutter_shopping/lib/flutter_shopping.dart +++ b/packages/flutter_shopping/lib/flutter_shopping.dart @@ -6,6 +6,7 @@ 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"; 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/models/product.dart b/packages/flutter_shopping/lib/src/models/product.dart new file mode 100644 index 0000000..680b5f5 --- /dev/null +++ b/packages/flutter_shopping/lib/src/models/product.dart @@ -0,0 +1,43 @@ +/// 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/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 52ca23f..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,18 +35,18 @@ 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. /// /// You can create your own implementation if you decide to use a different /// approach. -void onCompleteProductPage( +Future onCompleteProductPage( BuildContext context, -) { - context.go(FlutterShoppingPathRoutes.shoppingCart); +) async { + await context.push(FlutterShoppingPathRoutes.shoppingCart); } 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..d0b3cb2 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,186 @@ 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(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 60), + child: SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () async { + configuration.onCompleteUserStory.call(context); + }, + style: theme.filledButtonTheme.style?.copyWith( + backgroundColor: WidgetStateProperty.all( + theme.colorScheme.primary, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12, + ), + child: Text( + "Place another order", + style: theme.textTheme.displayLarge, + ), + ), + ), + ), + ), + ], ), ), ); } } + +Widget _discount(BuildContext context) { + 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/pubspec.yaml b/packages/flutter_shopping/pubspec.yaml index 762301a..ffbd981 100644 --- a/packages/flutter_shopping/pubspec.yaml +++ b/packages/flutter_shopping/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_shopping description: "A new Flutter project." -publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.0 +publish_to: 'none' +version: 2.0.0 environment: sdk: '>=3.3.4 <4.0.0' @@ -13,17 +13,17 @@ dependencies: flutter_product_page: git: url: https://github.com/Iconica-Development/flutter_shopping - ref: 1.0.0 + ref: 2.0.0 path: packages/flutter_product_page flutter_shopping_cart: git: url: https://github.com/Iconica-Development/flutter_shopping - ref: 1.0.0 + ref: 2.0.0 path: packages/flutter_shopping_cart flutter_order_details: git: url: https://github.com/Iconica-Development/flutter_shopping - ref: 1.0.0 + ref: 2.0.0 path: packages/flutter_order_details dev_dependencies: @@ -35,13 +35,4 @@ dev_dependencies: ref: 7.0.0 flutter: - # 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_cart/lib/flutter_shopping_cart.dart b/packages/flutter_shopping_cart/lib/flutter_shopping_cart.dart index b5fdb71..4b22672 100644 --- a/packages/flutter_shopping_cart/lib/flutter_shopping_cart.dart +++ b/packages/flutter_shopping_cart/lib/flutter_shopping_cart.dart @@ -3,6 +3,5 @@ library flutter_shopping_cart; export "src/config/shopping_cart_config.dart"; export "src/config/shopping_cart_localizations.dart"; -export "src/models/shopping_cart_product.dart"; export "src/services/product_service.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 852d124..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 @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; -import "package:flutter_shopping_cart/flutter_shopping_cart.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(); @@ -7,30 +8,21 @@ Widget _defaultNoContentBuilder(BuildContext context) => /// 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, this.confirmOrderButtonBuilder, this.confirmOrderButtonHeight = 100, - // this.sumBottomSheetBuilder, this.sumBottomSheetHeight = 100, - // - this.title, this.titleBuilder, - // this.localizations = const ShoppingCartLocalizations(), - // this.padding = const EdgeInsets.symmetric(horizontal: 32), this.bottomPadding = const EdgeInsets.fromLTRB(44, 0, 44, 32), - // this.appBar, - // - Widget Function(BuildContext context, Locale locale, T product)? - productItemBuilder, Widget Function(BuildContext context) noContentBuilder = _defaultNoContentBuilder, }) : assert( @@ -45,17 +37,7 @@ you cannot use the onConfirmOrder callback.""", If you do not override the confirm order button builder, you must use the onConfirmOrder callback.""", ), - _noContentBuilder = noContentBuilder { - _productItemBuilder = productItemBuilder; - _productItemBuilder ??= (context, locale, product) => ListTile( - title: Text(product.name), - subtitle: Text(product.price.toString()), - trailing: IconButton( - icon: const Icon(Icons.delete), - onPressed: () => productService.removeProduct(product), - ), - ); - } + _noContentBuilder = noContentBuilder; /// Product Service. The service contains all the products that /// a shopping cart can contain. Each product must extend the [Product] class. @@ -65,13 +47,15 @@ you must use the onConfirmOrder callback.""", /// support seperate shopping carts for shop. ProductService productService = ProductService([]); - late final Widget Function(BuildContext context, Locale locale, T product)? - _productItemBuilder; - /// Product item builder. This builder is used to build the product item /// that will be displayed in the shopping cart. - Widget Function(BuildContext context, Locale locale, T product) - get productItemBuilder => _productItemBuilder!; + final Widget Function( + BuildContext context, + Locale locale, + Product product, + ProductService productService, + ShoppingCartConfig configuration, + ) productItemBuilder; final Widget Function(BuildContext context) _noContentBuilder; @@ -115,10 +99,6 @@ you must use the onConfirmOrder callback.""", /// [sumBottomSheetBuilder] is overridden. final EdgeInsets bottomPadding; - /// Title of the shopping cart. The title is displayed at the top of the - /// shopping cart. If you provide a title builder, the title will be ignored. - final String? title; - /// 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. @@ -131,3 +111,99 @@ you must use the onConfirmOrder callback.""", /// 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), + ), + ], + ), + ], + ), + ), + ); +} diff --git a/packages/flutter_shopping_cart/lib/src/config/shopping_cart_localizations.dart b/packages/flutter_shopping_cart/lib/src/config/shopping_cart_localizations.dart index 19122d6..b1c793d 100644 --- a/packages/flutter_shopping_cart/lib/src/config/shopping_cart_localizations.dart +++ b/packages/flutter_shopping_cart/lib/src/config/shopping_cart_localizations.dart @@ -5,8 +5,10 @@ class ShoppingCartLocalizations { /// Creates shopping cart localizations const ShoppingCartLocalizations({ this.locale = const Locale("en", "US"), - this.placeOrder = "PLACE ORDER", - this.sum = "Total:", + this.placeOrder = "Order", + this.sum = "Subtotal:", + this.cartTitle = "Products", + this.close = "close", }); /// Locale for the shopping cart. @@ -21,4 +23,11 @@ class ShoppingCartLocalizations { /// Localization for the sum. final String sum; + + /// Title for the shopping cart. This title will be displayed at the top of + /// the shopping cart. + final String cartTitle; + + /// Localization for the close button for the popup. + final String close; } diff --git a/packages/flutter_shopping_cart/lib/src/models/shopping_cart_product.dart b/packages/flutter_shopping_cart/lib/src/models/shopping_cart_product.dart deleted file mode 100644 index 41043b0..0000000 --- a/packages/flutter_shopping_cart/lib/src/models/shopping_cart_product.dart +++ /dev/null @@ -1,29 +0,0 @@ -/// Abstract class for Product -/// -/// All products that want to be added to the shopping cart -/// must extend this class. -abstract class ShoppingCartProduct { - /// Creates a new product. - ShoppingCartProduct({ - required this.id, - required this.name, - required this.price, - this.quantity = 1, - }); - - /// Unique product identifier. - /// This identifier will be used to identify the product in the shopping cart. - /// If you don't provide an identifier, a random identifier will be generated. - final String id; - - /// Product name. - /// This name will be displayed in the shopping cart. - final String name; - - /// Product price. - /// This price will be displayed in the shopping cart. - final double price; - - /// Quantity for the product. - int quantity; -} diff --git a/packages/flutter_shopping_cart/lib/src/services/product_service.dart b/packages/flutter_shopping_cart/lib/src/services/product_service.dart index cb27ccc..a96989d 100644 --- a/packages/flutter_shopping_cart/lib/src/services/product_service.dart +++ b/packages/flutter_shopping_cart/lib/src/services/product_service.dart @@ -1,9 +1,9 @@ import "package:flutter/foundation.dart"; -import "package:flutter_shopping_cart/flutter_shopping_cart.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 { +class ProductService extends ChangeNotifier { /// Creates a product service. ProductService(this.products); 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 new file mode 100644 index 0000000..0c57633 --- /dev/null +++ b/packages/flutter_shopping_cart/lib/src/widgets/product_item_popup.dart @@ -0,0 +1,65 @@ +import "package:flutter/material.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; + +/// A popup that displays the product item. +class ProductItemPopup extends StatelessWidget { + /// Constructor for the product item popup. + const ProductItemPopup({ + required this.product, + required this.configuration, + super.key, + }); + + /// The product to display. + final Product product; + + /// Configuration for the product page. + final ShoppingCartConfig configuration; + + @override + Widget build(BuildContext context) { + var theme = Theme.of(context); + + return SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(32), + child: SizedBox( + width: double.infinity, + child: Column( + children: [ + Text( + product.description, + style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, + ), + Padding( + padding: const EdgeInsets.only(top: 20, left: 40, right: 40), + child: SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () => Navigator.of(context).pop(), + style: theme.filledButtonTheme.style?.copyWith( + backgroundColor: WidgetStateProperty.all( + theme.colorScheme.primary, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + child: Text( + configuration.localizations.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 1badb7e..4ffb093 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,9 +1,8 @@ import "package:flutter/material.dart"; -import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; +import "package:flutter_shopping/flutter_shopping.dart"; /// Shopping cart screen widget. -class ShoppingCartScreen - extends StatelessWidget { +class ShoppingCartScreen extends StatelessWidget { /// Creates a shopping cart screen. const ShoppingCartScreen({ required this.configuration, @@ -22,12 +21,19 @@ class ShoppingCartScreen children: [ if (configuration.titleBuilder != null) ...{ configuration.titleBuilder!(context), - } else if (configuration.title != null) ...{ + } else ...{ Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), - child: Text( - configuration.title!, - style: theme.textTheme.titleLarge, + padding: const EdgeInsets.symmetric( + vertical: 32, + ), + child: Row( + children: [ + Text( + configuration.localizations.cartTitle, + style: theme.textTheme.titleLarge, + textAlign: TextAlign.start, + ), + ], ), ), }, @@ -47,6 +53,8 @@ class ShoppingCartScreen 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. @@ -62,54 +70,36 @@ class ShoppingCartScreen ), ); - var bottomHeight = configuration.confirmOrderButtonHeight + - configuration.sumBottomSheetHeight; - - var bottomBlur = Container( - height: bottomHeight, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - theme.colorScheme.surface.withOpacity(0), - theme.colorScheme.surface.withOpacity(.5), - theme.colorScheme.surface.withOpacity(.8), - theme.colorScheme.surface.withOpacity(.8), - theme.colorScheme.surface.withOpacity(.8), - theme.colorScheme.surface.withOpacity(.8), - theme.colorScheme.surface.withOpacity(1), - ], - ), - ), - ); - return Scaffold( - appBar: configuration.appBar, - body: Stack( - fit: StackFit.expand, - children: [ - Padding( - padding: configuration.padding, - child: productBuilder, - ), - Align( - alignment: Alignment.bottomCenter, - child: bottomBlur, - ), - Align( - alignment: Alignment.bottomCenter, - child: _BottomSheet( - configuration: configuration, + appBar: configuration.appBar ?? + AppBar( + title: Text( + "Shopping cart", + style: theme.textTheme.headlineLarge, ), ), - ], + body: SafeArea( + child: Stack( + fit: StackFit.expand, + children: [ + Padding( + padding: configuration.padding, + child: productBuilder, + ), + Align( + alignment: Alignment.bottomCenter, + child: _BottomSheet( + configuration: configuration, + ), + ), + ], + ), ), ); } } -class _BottomSheet extends StatelessWidget { +class _BottomSheet extends StatelessWidget { const _BottomSheet({ required this.configuration, super.key, @@ -145,8 +135,7 @@ class _BottomSheet extends StatelessWidget { } } -class _DefaultConfirmOrderButton - extends StatelessWidget { +class _DefaultConfirmOrderButton extends StatelessWidget { const _DefaultConfirmOrderButton({ required this.configuration, }); @@ -169,26 +158,27 @@ class _DefaultConfirmOrderButton configuration.onConfirmOrder!(products); } - return SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 80), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: theme.colorScheme.primary, - ), + 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: 8.0, + vertical: 12, ), child: Text( - """${configuration.localizations.placeOrder} (${configuration.productService.countProducts()})""", - style: theme.textTheme.titleMedium?.copyWith( - color: theme.colorScheme.onPrimary, - ), + configuration.localizations.placeOrder, + style: theme.textTheme.displayLarge, ), ), ), @@ -222,8 +212,8 @@ class _DefaultSumBottomSheet extends StatelessWidget { ), const Spacer(), Text( - totalPrice.toStringAsFixed(2), - style: theme.textTheme.titleMedium, + "€ ${totalPrice.toStringAsFixed(2)}", + style: theme.textTheme.bodyMedium, ), ], ), diff --git a/packages/flutter_shopping_cart/pubspec.yaml b/packages/flutter_shopping_cart/pubspec.yaml index 119d182..1a4ad49 100644 --- a/packages/flutter_shopping_cart/pubspec.yaml +++ b/packages/flutter_shopping_cart/pubspec.yaml @@ -1,6 +1,7 @@ name: flutter_shopping_cart description: "A Flutter module for a shopping cart." -version: 1.0.0 +version: 2.0.0 +publish_to: 'none' environment: sdk: '>=3.3.0 <4.0.0' @@ -9,6 +10,12 @@ environment: dependencies: flutter: sdk: flutter + flutter_shopping: + git: + url: https://github.com/Iconica-Development/flutter_shopping + path: packages/flutter_shopping + ref: 2.0.0 + dev_dependencies: flutter_test: