Compare commits

...

4 commits

Author SHA1 Message Date
mike doornenbal
88cefb047b
feat: userstory (#13)
* feat: add interface to shopping cart

* feat: add interface to flutter_order_details

* feat: add interface to flutter_product_page

* feat: add userstory

* feat: cleanup configuration files

* feat: add builders for productpage

* feat: add filter screen and widgets

* feat: remove listenablebuilders from cart

* feat: remove listenablebuilders from order details

* feat: remove listenablebuilder from product page

---------

Co-authored-by: mike doornenbal <mikedoornenbal9@icloud.com>
2024-07-12 09:49:58 +02:00
mike doornenbal
fd8afbde03
feat: add interface and local service (#11)
Co-authored-by: mike doornenbal <mikedoornenbal9@icloud.com>
2024-07-03 11:46:29 +02:00
mike doornenbal
ee377c107c
feat: shopping interface (#10)
Co-authored-by: mike doornenbal <mikedoornenbal9@icloud.com>
2024-07-02 14:31:41 +02:00
mike doornenbal
5a24f7cf6f
fix: design changes (#9)
* fix: shopping cart screen

* fix: product page

* fix: add popup to shopping cart

* fix: order detail screens

* fix: button styling

* fix: dependency

---------

Co-authored-by: mike doornenbal <mikedoornenbal9@icloud.com>
2024-07-02 13:39:36 +02:00
127 changed files with 3561 additions and 4890 deletions

View file

@ -1,3 +1,8 @@
## 2.0.0
- Added `flutter_shopping_interface` package
- Implemented default design
## 1.0.0
- Initial version of the combined melos variant of the flutter_shopping user-story.

View file

@ -1,17 +1,9 @@
/// Flutter component for shopping cart.
library flutter_order_details;
export "package:flutter_form_wizard/flutter_form.dart";
export "src/configuration/order_detail_configuration.dart";
export "src/configuration/order_detail_localization.dart";
export "src/configuration/order_detail_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";
export "src/configuration/order_detail_translations.dart";
export "src/order_detail_screen.dart";
export "src/widgets/order_succes.dart";

View file

@ -1,50 +1,71 @@
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";
import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Configuration for the order detail screen.
class OrderDetailConfiguration {
/// Constructor for the order detail configuration.
const OrderDetailConfiguration({
required this.steps,
//
required this.onCompleted,
//
this.progressIndicator = true,
//
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,
required this.shoppingService,
required this.onNextStep,
required this.onStepsCompleted,
required this.onCompleteOrderDetails,
this.pages,
this.translations = const OrderDetailTranslations(),
this.appBarBuilder,
this.nextbuttonBuilder,
this.orderSuccessBuilder,
});
/// The shopping service that is used
final ShoppingService shoppingService;
/// The different steps that the user has to go through to complete the order.
/// Each step contains a list of fields that the user has to fill in.
final List<OrderDetailStep> steps;
final List<FlutterFormPage> 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;
final Function(
String shopId,
List<Product> products,
Map<int, Map<String, dynamic>> value,
OrderDetailConfiguration configuration,
) onStepsCompleted;
/// Whether or not you want to show a progress indicator at
/// the top of the screen.
final bool progressIndicator;
/// Callback function that is called when the user has completed a step.
final Function(
int currentStep,
Map<String, dynamic> data,
FlutterFormController controller,
) onNextStep;
/// 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;
final OrderDetailTranslations translations;
/// Optional app bar that you can pass to the order detail screen.
final PreferredSizeWidget? appBar;
final PreferredSizeWidget? Function(BuildContext context, String title)?
appBarBuilder;
/// Optional next button builder that you can pass to the order detail screen.
final Widget Function(
int currentStep,
// ignore: avoid_positional_boolean_parameters
bool checkingPages,
BuildContext context,
OrderDetailConfiguration configuration,
FlutterFormController controller,
)? nextbuttonBuilder;
/// Optional builder for the order success screen.
final Widget Function(
BuildContext context,
OrderDetailConfiguration,
Map<int, Map<String, dynamic>> orderDetails,
)? orderSuccessBuilder;
/// This function is called after the order has been completed and
/// the success screen has been shown.
final Function(BuildContext context, OrderDetailConfiguration configuration)
onCompleteOrderDetails;
}

View file

@ -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<FormState> formKey;
/// List of fields that the user has to fill in.
/// Each field must extend from the `OrderDetailInput` class.
final List<OrderDetailInput> fields;
}

View file

@ -1,14 +0,0 @@
/// An enum to define the style of the title in the order detail.
enum OrderDetailTitleStyle {
/// The title displayed as a textlabel above the field.
text,
/// The title displayed as a label inside the field.
/// NOTE: Not all fields support this. Such as, but not limited to:
/// - Dropdown
/// - Time Picker
label,
/// Does not display any form of title.
none,
}

View file

@ -1,18 +1,18 @@
/// Localizations for the order detail page.
class OrderDetailLocalization {
class OrderDetailTranslations {
/// Constructor for the order detail localization.
const OrderDetailLocalization({
this.nextButton = "Next",
this.backButton = "Back",
const OrderDetailTranslations({
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;
}

View file

@ -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,
),
);
}
}

View file

@ -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<String> {
/// 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<String? Function(String?)>? streetNameValidators;
/// Validators for the postal code.
final List<String? Function(String?)>? postalCodeValidators;
/// Validators for the city.
final List<String? Function(String?)>? cityValidators;
/// Input formatters for the postal code.
final List<TextInputFormatter>? 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,
);
}
}

View file

@ -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<String> {
/// 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<String> 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<T> extends FormField<T> {
_ChoiceInputField({
required T currentValue,
required List<T> 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<T> field) => Padding(
padding: fieldPadding,
child: Column(
children: [
for (var item in items) ...[
Padding(
padding: paddingBetweenFields,
child: _InputContent<T>(
i: item,
currentValue: currentValue,
onTap: onTap,
),
),
],
if (field.hasError) ...[
FormFieldErrorBuilder(errorMessage: field.errorText!),
],
],
),
),
);
}
class _InputContent<T> 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,
);
}
}

View file

@ -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<T> extends OrderDetailInput<T> {
/// 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<T> 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<T>(
value: currentValue ?? initialValue ?? buildInitialValue ?? items[0],
selectedItemBuilder: (context) => items
.map(
(item) => Text(
item.toString(),
style: theme.textTheme.labelMedium,
),
)
.toList(),
items: items
.map(
(item) => DropdownMenuItem<T>(
value: item,
child: _DropdownButtonBuilder<T>(
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<T> 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,
],
],
),
);
}
}

View file

@ -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<String> {
/// 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,
);
}
}

View file

@ -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<T> {
/// 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<String? Function(T?)> 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<FormField>) ...[
Column(
children: child
.map(
(FormField field) => Padding(
padding: paddingBetweenFields,
child: field,
),
)
.toList(),
),
] else if (child is List<OrderDetailInput>) ...[
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,
);
}

View file

@ -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<String> {
/// 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,
);
}
}

View file

@ -1,14 +0,0 @@
/// OrderResult model.
/// When an user completes the field and presses the complete button,
/// the `onComplete` method returns an instance of this class that contains
/// all the developer-specified `outputKey`s and the value that was provided
/// by the user.
class OrderResult {
/// Constructor of the order result class.
OrderResult({
required this.order,
});
/// Map of `outputKey`s and their respected values.
final Map<String, dynamic> order;
}

View file

@ -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<String> {
/// 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<TextInputFormatter> 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,
);
}
}

View file

@ -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<String> {
/// 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<String>(
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<T> extends FormField<T> {
_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<T> 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<T> 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;
}

View file

@ -0,0 +1,68 @@
import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
import "package:flutter_order_details/src/widgets/default_appbar.dart";
import "package:flutter_order_details/src/widgets/default_next_button.dart";
import "package:flutter_order_details/src/widgets/default_order_detail_pages.dart";
/// Order Detail Screen.
class OrderDetailScreen extends StatefulWidget {
/// Screen that builds all forms based on the configuration.
const OrderDetailScreen({
required this.configuration,
super.key,
});
/// Configuration for the screen.
final OrderDetailConfiguration configuration;
@override
State<OrderDetailScreen> createState() => _OrderDetailScreenState();
}
class _OrderDetailScreenState extends State<OrderDetailScreen> {
@override
Widget build(BuildContext context) {
var controller = FlutterFormController();
return Scaffold(
appBar: widget.configuration.appBarBuilder?.call(
context,
widget.configuration.translations.orderDetailsTitle,
) ??
DefaultAppbar(
title: widget.configuration.translations.orderDetailsTitle,
),
body: FlutterForm(
formController: controller,
options: FlutterFormOptions(
nextButton: (pageNumber, checkingPages) =>
widget.configuration.nextbuttonBuilder?.call(
pageNumber,
checkingPages,
context,
widget.configuration,
controller,
) ??
DefaultNextButton(
controller: controller,
configuration: widget.configuration,
currentStep: pageNumber,
checkingPages: checkingPages,
),
pages: widget.configuration.pages?.call(context) ??
defaultPages(context, () {
setState(() {});
}),
onFinished: (data) async {
widget.configuration.onStepsCompleted.call(
widget.configuration.shoppingService.shopService.selectedShop!.id,
widget.configuration.shoppingService.shoppingCartService.products,
data,
widget.configuration,
);
},
onNext: (step, data) {},
),
),
);
}
}

View file

@ -0,0 +1,27 @@
import "package:flutter/material.dart";
/// Default appbar for the order details page.
class DefaultAppbar extends StatelessWidget implements PreferredSizeWidget {
/// Constructor for the default appbar for the order details page.
const DefaultAppbar({
required this.title,
super.key,
});
/// Title of the appbar.
final String title;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return AppBar(
title: Text(
title,
style: theme.textTheme.headlineLarge,
),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

View file

@ -0,0 +1,69 @@
import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
/// Default next button for the order details page.
class DefaultNextButton extends StatelessWidget {
/// Constructor for the default next button for the order details page.
const DefaultNextButton({
required this.controller,
required this.configuration,
required this.currentStep,
required this.checkingPages,
super.key,
});
/// Configuration for the order details page.
final OrderDetailConfiguration configuration;
/// Controller for the form.
final FlutterFormController controller;
/// Current step in the form.
final int currentStep;
/// Whether the form is checking pages.
final bool checkingPages;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var nextButtonTexts = [
"Choose date and time",
"Next",
"Next",
];
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 32),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () async {
configuration.onNextStep(
currentStep,
controller.getCurrentStepResults(),
controller,
);
},
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12,
),
child: Text(
nextButtonTexts[currentStep],
style: theme.textTheme.displayLarge,
),
),
),
),
),
);
}
}

View file

@ -0,0 +1,431 @@
import "package:animated_toggle/animated_toggle.dart";
import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
/// Default pages for the order details screen.
List<FlutterFormPage> defaultPages(
BuildContext context,
Function() onSwitched,
) {
var theme = Theme.of(context);
var morningTimes = <String>[
"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 = <String>[
"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<bool>(false);
var multipleChoiceController = FlutterFormInputMultipleChoiceController(
id: "multipleChoice",
mandatory: true,
);
return [
FlutterFormPage(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"What's your name?",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: inputDecoration("Name"),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "name",
mandatory: true,
),
validationMessage: "Please enter your name",
),
const SizedBox(
height: 16,
),
Text(
"What's your address?",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: inputDecoration("Street and number"),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "street",
mandatory: true,
),
validationMessage: "Please enter your address",
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter a street and house number";
}
var regex = RegExp(r"^[A-Za-z]+\s[0-9]{1,3}$");
if (!regex.hasMatch(value)) {
return "Invalid street and house number";
}
return null;
},
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: inputDecoration("Postal code"),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "postalCode",
mandatory: true,
),
validationMessage: "Please enter your postal code",
validator: (value) {
if (value == null || value.isEmpty) {
return "Please enter a postal code";
}
var regex = RegExp(r"^[0-9]{4}[A-Za-z]{2}$");
if (!regex.hasMatch(value)) {
return "Invalid postal code format";
}
return null;
},
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: inputDecoration("City"),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "city",
mandatory: true,
),
validationMessage: "Please enter your city",
),
const SizedBox(
height: 16,
),
Text(
"What's your phone number?",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputPhone(
numberFieldStyle: theme.textTheme.bodySmall,
textAlignVertical: TextAlignVertical.center,
decoration: inputDecoration("Phone number"),
controller: FlutterFormInputPhoneController(
id: "phone",
mandatory: true,
),
validationMessage: "Please enter your phone number",
validator: (value) {
if (value == null || value.number!.isEmpty) {
return "Please enter a phone number";
}
// Remove any spaces or hyphens from the input
var phoneNumber =
value.number!.replaceAll(RegExp(r"\s+|-"), "");
// Check the length of the remaining digits
if (phoneNumber.length != 10 && phoneNumber.length != 11) {
return "Invalid phone number length";
}
// Check if all remaining characters are digits
if (!phoneNumber.substring(1).contains(RegExp(r"^[0-9]*$"))) {
return "Phone number can only contain digits";
}
// If all checks pass, return null (no error)
return null;
},
),
const SizedBox(
height: 16,
),
Text(
"What's your email address?",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputEmail(
style: theme.textTheme.bodySmall,
decoration: inputDecoration("email address"),
controller: FlutterFormInputEmailController(
id: "email",
mandatory: true,
),
validationMessage: "Please fill in a valid email address",
),
const SizedBox(
height: 16,
),
Text(
"Do you have any comments?",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: inputDecoration("Optional"),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "comments",
),
validationMessage: "Please enter your email address",
),
const SizedBox(
height: 100,
),
],
),
),
),
),
FlutterFormPage(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(
"When and at what time would you like to pick up your order?",
style: theme.textTheme.titleMedium,
),
),
const SizedBox(
height: 4,
),
FlutterFormInputDropdown(
icon: const Icon(
Icons.keyboard_arrow_down,
color: Colors.black,
),
isDense: true,
decoration: dropdownInputDecoration("Select a day"),
validationMessage: "Please select a day",
controller: FlutterFormInputDropdownController(
id: "date",
mandatory: true,
),
items: [
DropdownMenuItem(
value: "Today",
child: Text(
"Today",
style: theme.textTheme.bodySmall,
),
),
DropdownMenuItem(
value: "Tomorrow",
child: Text(
"Tomorrow",
style: theme.textTheme.bodySmall,
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: AnimatedToggle(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
blurRadius: 5,
color: theme.colorScheme.primary.withOpacity(0.8),
),
],
color: Colors.white,
borderRadius: BorderRadius.circular(50),
),
width: 280,
toggleColor: theme.colorScheme.primary,
onSwitch: (value) {
switchStatus.value = value;
onSwitched();
},
childLeft: Center(
child: Text(
"Morning",
style: theme.textTheme.titleSmall?.copyWith(
color: switchStatus.value
? theme.colorScheme.primary
: Colors.white,
),
),
),
childRight: Center(
child: Text(
"Afternoon",
style: theme.textTheme.titleSmall?.copyWith(
color: switchStatus.value
? Colors.white
: theme.colorScheme.primary,
),
),
),
),
),
const SizedBox(
height: 8,
),
FlutterFormInputMultipleChoice(
validationMessage: "Select a Time",
controller: multipleChoiceController,
options: switchStatus.value ? afternoonTimes : morningTimes,
mainAxisSpacing: 5,
crossAxisSpacing: 5,
childAspectRatio: 2,
height: MediaQuery.of(context).size.height * 0.6,
builder: (context, index, selected, controller, options, state) =>
GestureDetector(
onTap: () {
state.didChange(options[index]);
selected.value = index;
controller.onSaved(options[index]);
},
child: Container(
decoration: BoxDecoration(
color: selected.value == index
? Theme.of(context).colorScheme.primary
: Colors.white,
borderRadius: BorderRadius.circular(10),
),
height: 40,
child: Center(
child: Text(options[index]),
),
),
),
),
],
),
),
),
FlutterFormPage(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Payment method",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
Text(
"Choose when you would like to to pay for the order.",
style: theme.textTheme.bodyMedium,
),
const SizedBox(
height: 84,
),
FlutterFormInputMultipleChoice(
crossAxisCount: 1,
mainAxisSpacing: 24,
crossAxisSpacing: 5,
childAspectRatio: 2,
height: 420,
controller: FlutterFormInputMultipleChoiceController(
id: "payment",
mandatory: true,
),
options: const ["PAY NOW", "PAY AT THE CASHIER"],
builder: (context, index, selected, controller, options, state) =>
GestureDetector(
onTap: () {
state.didChange(options[index]);
selected.value = index;
controller.onSaved(options[index]);
},
child: Container(
decoration: BoxDecoration(
color: selected.value == index
? Theme.of(context).colorScheme.primary
: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Theme.of(context).colorScheme.primary,
),
),
height: 40,
child: Center(child: Text(options[index])),
),
),
validationMessage: "Please select a payment method",
),
],
),
),
),
];
}

View file

@ -1,273 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
/// Order Detail Screen.
class OrderDetailScreen extends StatefulWidget {
/// Screen that builds all forms based on the configuration.
const OrderDetailScreen({
required this.configuration,
super.key,
});
/// Configuration for the screen.
final OrderDetailConfiguration configuration;
@override
State<OrderDetailScreen> createState() => _OrderDetailScreenState();
}
class _OrderDetailScreenState extends State<OrderDetailScreen> {
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),
),
),
);
return Scaffold(
appBar: widget.configuration.appBar,
body: Stack(
children: [
pageBody,
if (_blurBackground) pageBlur,
],
),
);
}
}
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,
),
),
],
);
}
}

View file

@ -0,0 +1,222 @@
import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Default order success widget.
class DefaultOrderSucces extends StatelessWidget {
/// Constructor for the DefaultOrderSucces.
const DefaultOrderSucces({
required this.configuration,
required this.orderDetails,
super.key,
});
/// Configuration for the user-stor
final OrderDetailConfiguration configuration;
/// Order details.
final Map<int, Map<String, dynamic>> orderDetails;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var discountedProducts = configuration
.shoppingService.productService.products
.where((product) => product.hasDiscount)
.toList();
return Scaffold(
appBar: AppBar(
title: Text(
"Confirmation",
style: theme.textTheme.headlineLarge,
),
),
body: SafeArea(
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
left: 32,
top: 32,
right: 32,
),
child: Column(
children: [
Text(
"Success!",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
Text(
"Thank you ${orderDetails[0]!['name']} for your order!",
style: theme.textTheme.bodyMedium,
),
const SizedBox(
height: 16,
),
Text(
"The order was placed"
// ignore: lines_longer_than_80_chars
" at ${configuration.shoppingService.shopService.selectedShop?.name}."
" You can pick this"
" up ${orderDetails[1]!['date']} at"
" ${orderDetails[1]!['multipleChoice']}.",
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(
height: 16,
),
Text(
"If you want, you can place another order in this street.",
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(
height: 32,
),
Text(
"Weekly offers",
style: theme.textTheme.headlineSmall
?.copyWith(color: Colors.black),
),
const SizedBox(
height: 4,
),
],
),
),
SizedBox(
height: 272,
child: ListView(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: [
const SizedBox(width: 32),
// _discount(context),
// const SizedBox(width: 8),
// _discount(context),
for (var product in discountedProducts) ...[
_discount(
context,
product,
configuration.shoppingService.shopService.selectedShop!,
),
const SizedBox(
width: 8,
),
],
const SizedBox(width: 32),
],
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () async {
configuration.onCompleteOrderDetails
.call(context, configuration);
},
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12,
),
child: Text(
"Place another order",
style: theme.textTheme.displayLarge,
),
),
),
),
),
],
),
),
);
}
}
Widget _discount(BuildContext context, Product product, Shop shop) {
var theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.black,
),
borderRadius: BorderRadius.circular(10),
),
width: MediaQuery.of(context).size.width - 64,
height: 200,
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(
10,
),
child: Image.network(
product.imageUrl,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
),
Container(
alignment: Alignment.centerLeft,
height: 38,
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
),
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
shop.name,
style: theme.textTheme.headlineSmall?.copyWith(
color: Colors.white,
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
alignment: Alignment.centerLeft,
width: MediaQuery.of(context).size.width,
height: 68,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
"${product.name}, now for ${product.price.toStringAsFixed(2)}",
style: theme.textTheme.bodyMedium,
),
),
),
),
],
),
);
}

View file

@ -1,13 +1,28 @@
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'
sdk: ">=3.3.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
animated_toggle:
git:
url: https://github.com/Iconica-Development/flutter_animated_toggle
ref: 0.0.3
flutter_form_wizard:
git:
url: https://github.com/Iconica-Development/flutter_form_wizard
ref: 6.5.0
flutter_shopping_interface:
git:
url: https://github.com/Iconica-Development/flutter_shopping
path: packages/flutter_shopping_interface
ref: 2.0.0
collection: ^1.18.0
dev_dependencies:
flutter_test:
@ -18,13 +33,3 @@ 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

View file

@ -2,12 +2,7 @@
/// detailed view of each product.
library flutter_product_page;
export "src/configuration/product_page_category_styling_configuration.dart";
export "src/configuration/product_page_configuration.dart";
export "src/configuration/product_page_content.dart";
export "src/configuration/product_page_localization.dart";
export "src/configuration/product_page_shop_selector_style.dart";
export "src/models/product.dart";
export "src/models/product_page_shop.dart";
export "src/ui/product_page.dart";
export "src/ui/product_page_screen.dart";
export "src/configuration/product_page_translations.dart";
export "src/product_page_screen.dart";

View file

@ -0,0 +1,67 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
/// Category selection screen.
class CategorySelectionScreen extends StatelessWidget {
/// Constructor for the category selection screen.
const CategorySelectionScreen({
required this.configuration,
super.key,
});
/// Configuration for the product page.
final ProductPageConfiguration configuration;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
leading: const SizedBox.shrink(),
title: Text(
"filter",
style: theme.textTheme.headlineLarge,
),
actions: [
IconButton(
onPressed: () async {
Navigator.of(context).pop();
},
icon: const Icon(Icons.close),
),
],
),
body: ListenableBuilder(
listenable: configuration.shoppingService.productService,
builder: (context, _) => Column(
children: [
...configuration.shoppingService.productService.getCategories().map(
(category) {
var isChecked = configuration
.shoppingService.productService.selectedCategories
.contains(category);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CheckboxListTile(
activeColor: theme.colorScheme.primary,
controlAffinity: ListTileControlAffinity.leading,
value: isChecked,
onChanged: (value) {
configuration.shoppingService.productService
.selectCategory(category);
},
shape: const UnderlineInputBorder(),
title: Text(
category,
style: theme.textTheme.bodyMedium,
),
),
);
},
),
],
),
),
);
}
}

View file

@ -1,54 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_nested_categories/flutter_nested_categories.dart"
show CategoryHeaderStyling;
/// Configuration for the styling of the category list on the product page.
/// This configuration allows to customize the title, header styling and
/// the collapsible behavior of the categories.
class ProductPageCategoryStylingConfiguration {
/// Constructor to create a new instance of
/// [ProductPageCategoryStylingConfiguration].
const ProductPageCategoryStylingConfiguration({
this.headerStyling,
this.headerCentered = false,
this.customTitle,
this.title,
this.titleStyle,
this.titleCentered = false,
this.isCategoryCollapsible = true,
});
/// Optional title for the category list. This will be displayed at the
/// top of the list.
final String? title;
/// Optional custom title widget for the category list. This will be
/// displayed at the top of the list. If set, the text title will be
/// ignored.
final Widget? customTitle;
/// Optional title style for the title of the category list. This will
/// be applied to the title of the category list. If not set, the default
/// text style will be used.
final TextStyle? titleStyle;
/// Configure if the title should be centered.
///
/// Default is false.
final bool titleCentered;
/// Optional header styling for the categories. This will be applied to
/// the name of the categories. If not set, the default text style will
/// be used.
final CategoryHeaderStyling? headerStyling;
/// Configure if the category header should be centered.
///
/// Default is false.
final bool headerCentered;
/// Configure if the category should be collapsible.
///
/// Default is true.
final bool isCategoryCollapsible;
}

View file

@ -1,185 +1,96 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_product_page/src/ui/widgets/product_item_popup.dart";
import "package:flutter_product_page/src/widgets/product_item_popup.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Configuration for the product page.
class ProductPageConfiguration {
/// Constructor for the product page configuration.
ProductPageConfiguration({
required this.shoppingService,
required this.shops,
//
required this.getProducts,
//
required this.onAddToCart,
required this.onNavigateToShoppingCart,
this.navigateToShoppingCartBuilder,
//
required this.getProductsInShoppingCart,
this.shoppingCartButtonBuilder,
this.initialShopId,
//
this.productBuilder,
//
this.onShopSelectionChange,
this.getProductsInShoppingCart,
//
this.localizations = const ProductPageLocalization(),
//
this.shopSelectorStyle = ShopSelectorStyle.spacedWrap,
this.categoryStylingConfiguration =
const ProductPageCategoryStylingConfiguration(),
//
this.translations = const ProductPageTranslations(),
this.shopSelectorStyle = ShopSelectorStyle.row,
this.pagePadding = const EdgeInsets.all(4),
//
this.appBar,
this.appBarBuilder,
this.bottomNavigationBar,
//
Function(
BuildContext context,
ProductPageProduct product,
)? onProductDetail,
String Function(
ProductPageProduct product,
)? getDiscountDescription,
Widget Function(
BuildContext context,
ProductPageProduct product,
)? productPopupBuilder,
Widget Function(
BuildContext context,
)? noContentBuilder,
Widget Function(
BuildContext context,
Object? error,
StackTrace? stackTrace,
)? errorBuilder,
this.onProductDetail,
this.discountDescription,
this.noContentBuilder,
this.errorBuilder,
this.shopselectorBuilder,
this.discountBuilder,
this.categoryListBuilder,
this.selectedCategoryBuilder,
}) {
_productPopupBuilder = productPopupBuilder;
_productPopupBuilder ??=
(BuildContext context, ProductPageProduct product) => ProductItemPopup(
product: product,
configuration: this,
);
_onProductDetail = onProductDetail;
_onProductDetail ??=
(BuildContext context, ProductPageProduct product) async {
var theme = Theme.of(context);
await showModalBottomSheet(
context: context,
backgroundColor: theme.colorScheme.surface,
builder: (context) => _productPopupBuilder!(
context,
product,
),
);
};
_noContentBuilder = noContentBuilder;
_noContentBuilder ??= (BuildContext context) {
var theme = Theme.of(context);
return Center(
child: Text(
"No content",
style: theme.textTheme.titleLarge,
),
);
};
_errorBuilder = errorBuilder;
_errorBuilder ??=
(BuildContext context, Object? error, StackTrace? stackTrace) {
var theme = Theme.of(context);
return Center(
child: Text(
"Error: $error",
style: theme.textTheme.titleLarge,
),
);
};
_getDiscountDescription = getDiscountDescription;
_getDiscountDescription ??=
(ProductPageProduct product) => "${product.name} is on sale!";
onProductDetail ??= _onProductDetail;
discountDescription ??= _defaultDiscountDescription;
}
/// The shopping service that is used
final ShoppingService shoppingService;
/// The shop that is initially selected.
final String? initialShopId;
/// A list of all the shops that the user must be able to navigate from.
final Future<List<ProductPageShop>> shops;
final Future<List<Shop>> Function() shops;
/// A function that returns all the products that belong to a certain shop.
/// The function must return a [ProductPageContent] object.
final Future<ProductPageContent> Function(ProductPageShop shop) getProducts;
/// The function must return a [List<Product>].
final Future<List<Product>> Function(Shop shop) getProducts;
/// The localizations for the product page.
final ProductPageLocalization localizations;
final ProductPageTranslations translations;
/// Builder for the product item. These items will be displayed in the list
/// for each product in their seperated category. This builder should only
/// build the widget for one specific product. This builder has a default
/// in-case the developer does not override it.
Widget Function(BuildContext context, ProductPageProduct product)?
productBuilder;
final Widget Function(
BuildContext context,
Product product,
ProductPageConfiguration configuration,
)? productBuilder;
late Widget Function(BuildContext context, ProductPageProduct 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)
get productPopupBuilder => _productPopupBuilder!;
late Function(BuildContext context, ProductPageProduct 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!;
late Widget Function(BuildContext context)? _noContentBuilder;
/// The no content builder is used when a shop has no products. This builder
/// has a default in-case the developer does not override it.
Function(BuildContext context)? get noContentBuilder => _noContentBuilder;
/// The builder for the product popup. This builder should return a widget
Function(
BuildContext context,
Product product,
String closeText,
)? onProductDetail;
/// The builder for the shopping cart. This builder should return a widget
/// that navigates to the shopping cart overview page.
Widget Function(BuildContext context)? navigateToShoppingCartBuilder;
late Widget Function(
final Widget Function(
BuildContext context,
Object? error,
StackTrace? stackTrace,
)? _errorBuilder;
ProductPageConfiguration configuration,
)? shoppingCartButtonBuilder;
/// The error builder is used when an error occurs. This builder has a default
/// in-case the developer does not override it.
Widget Function(BuildContext context, Object? error, StackTrace? stackTrace)?
get errorBuilder => _errorBuilder;
late String Function(ProductPageProduct 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 =>
_getDiscountDescription!;
/// The function that returns the discount description for a product.
String Function(
Product product,
)? discountDescription;
/// This function must be implemented by the developer and should handle the
/// adding of a product to the cart.
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.
final Function(ProductPageShop shop)? onShopSelectionChange;
final Function(Shop shop)? onShopSelectionChange;
/// This function must be implemented by the developer and should handle the
/// navigation to the shopping cart overview page.
final int Function()? getProductsInShoppingCart;
final int Function() getProductsInShoppingCart;
/// This function must be implemented by the developer and should handle the
/// navigation to the shopping cart overview page.
@ -188,9 +99,6 @@ class ProductPageConfiguration {
/// The style of the shop selector.
final ShopSelectorStyle shopSelectorStyle;
/// The styling configuration for the category list.
final ProductPageCategoryStylingConfiguration categoryStylingConfiguration;
/// The padding for the page.
final EdgeInsets pagePadding;
@ -198,5 +106,68 @@ class ProductPageConfiguration {
final Widget? bottomNavigationBar;
/// Optional app bar that you can pass to the order detail screen.
final PreferredSizeWidget? appBar;
final PreferredSizeWidget Function(BuildContext context)? appBarBuilder;
/// Builder for the no content widget. This builder is used when there is no
/// content to display.
final Widget Function(
BuildContext context,
)? noContentBuilder;
/// Builder for the error widget. This builder is used when there is an error
/// to display.
final Widget Function(
BuildContext context,
Object? error,
)? errorBuilder;
/// Builder for the shop selector. This builder is used to build the shop
/// selector that will be displayed in the product page.
final Widget Function(
BuildContext context,
ProductPageConfiguration configuration,
List<Shop> shops,
Function(Shop shop) onShopSelectionChange,
)? shopselectorBuilder;
/// Builder for the discount widget. This builder is used to build the
/// discount widget that will be displayed in the product page.
final Widget Function(
BuildContext context,
ProductPageConfiguration configuration,
List<Product> discountedProducts,
)? discountBuilder;
/// Builder for the list of items that are displayed in the product page.
final Widget Function(
BuildContext context,
ProductPageConfiguration configuration,
List<Product> products,
)? categoryListBuilder;
/// Builder for the list of selected categories
final Widget Function(ProductPageConfiguration configuration)?
selectedCategoryBuilder;
}
Future<void> _onProductDetail(
BuildContext context,
Product product,
String closeText,
) async {
var theme = Theme.of(context);
await showModalBottomSheet(
context: context,
backgroundColor: theme.colorScheme.surface,
builder: (context) => ProductItemPopup(
product: product,
closeText: closeText,
),
);
}
String _defaultDiscountDescription(
Product product,
) =>
"${product.name}, now for ${product.discountPrice} each";

View file

@ -1,16 +0,0 @@
import "package:flutter_product_page/flutter_product_page.dart";
/// Return type that contains the products and an optional discounted product.
class ProductPageContent {
/// Default constructor for this class.
const ProductPageContent({
required this.products,
this.discountedProduct,
});
/// List of products that belong to the shop.
final List<ProductPageProduct> products;
/// Optional highlighted discounted product to display.
final ProductPageProduct? discountedProduct;
}

View file

@ -1,11 +1,13 @@
/// Localization for the product page
class ProductPageLocalization {
class ProductPageTranslations {
/// Default constructor
const ProductPageLocalization({
this.navigateToShoppingCart = "To shopping cart",
this.discountTitle = "Discount",
const ProductPageTranslations({
this.navigateToShoppingCart = "View shopping cart",
this.discountTitle = "Weekly offer",
this.failedToLoadImageExplenation = "Failed to load image",
this.close = "Close",
this.categoryItemListTitle = "What would you like to order",
this.appBarTitle = "ProductPage",
});
/// Message to navigate to the shopping cart
@ -19,4 +21,10 @@ class ProductPageLocalization {
/// Close button for the product page
final String close;
/// Title for the category item list
final String categoryItemListTitle;
/// Title for the app bar
final String appBarTitle;
}

View file

@ -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;
}

View file

@ -1,18 +0,0 @@
/// The product page shop class contains all the required information
/// that needs to be known about a certain shop.
///
/// In your own implemententation, you must extend from this class so you can
/// add more fields to this class to suit your needs.
class ProductPageShop {
/// The default constructor for this class.
const ProductPageShop({
required this.id,
required this.name,
});
/// The unique identifier for the shop.
final String id;
/// The name of the shop.
final String name;
}

View file

@ -0,0 +1,306 @@
import "package:collection/collection.dart";
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_product_page/src/services/category_service.dart";
import "package:flutter_product_page/src/widgets/defaults/default_appbar.dart";
import "package:flutter_product_page/src/widgets/defaults/default_error.dart";
import "package:flutter_product_page/src/widgets/defaults/default_no_content.dart";
import "package:flutter_product_page/src/widgets/defaults/default_shopping_cart_button.dart";
import "package:flutter_product_page/src/widgets/defaults/selected_categories.dart";
import "package:flutter_product_page/src/widgets/shop_selector.dart";
import "package:flutter_product_page/src/widgets/weekly_discount.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// A page that displays products.
class ProductPageScreen extends StatefulWidget {
/// Constructor for the product page.
const ProductPageScreen({
required this.configuration,
super.key,
});
/// Configuration for the product page.
final ProductPageConfiguration configuration;
@override
State<ProductPageScreen> createState() => _ProductPageScreenState();
}
class _ProductPageScreenState extends State<ProductPageScreen> {
@override
Widget build(BuildContext context) => Scaffold(
appBar: widget.configuration.appBarBuilder?.call(context) ??
DefaultAppbar(
configuration: widget.configuration,
),
bottomNavigationBar: widget.configuration.bottomNavigationBar,
body: SafeArea(
child: Padding(
padding: widget.configuration.pagePadding,
child: FutureBuilder(
// ignore: discarded_futures
future: widget.configuration.shops(),
builder: (context, snapshot) {
List<Shop>? shops;
if (snapshot.connectionState == ConnectionState.waiting) {
return const Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(child: CircularProgressIndicator.adaptive()),
],
);
}
if (snapshot.hasError) {
return widget.configuration.errorBuilder?.call(
context,
snapshot.error,
) ??
DefaultError(
error: snapshot.error,
);
}
shops = snapshot.data;
if (shops == null || shops.isEmpty) {
return widget.configuration.errorBuilder?.call(
context,
snapshot.error,
) ??
DefaultError(error: snapshot.error);
}
if (widget.configuration.initialShopId != null) {
var initialShop = shops.firstWhereOrNull(
(shop) => shop.id == widget.configuration.initialShopId,
);
if (initialShop != null) {
widget.configuration.shoppingService.shopService.selectShop(
initialShop,
);
} else {
widget.configuration.shoppingService.shopService.selectShop(
shops.first,
);
}
} else {
widget.configuration.shoppingService.shopService.selectShop(
shops.first,
);
}
return _ProductPageContent(
configuration: widget.configuration,
shops: shops,
);
},
),
),
),
);
}
class _ProductPageContent extends StatefulWidget {
const _ProductPageContent({
required this.configuration,
required this.shops,
});
final ProductPageConfiguration configuration;
final List<Shop> shops;
@override
State<_ProductPageContent> createState() => _ProductPageContentState();
}
class _ProductPageContentState extends State<_ProductPageContent> {
@override
Widget build(BuildContext context) => Stack(
children: [
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// shop selector
widget.configuration.shopselectorBuilder?.call(
context,
widget.configuration,
widget.shops,
widget
.configuration.shoppingService.shopService.selectShop,
) ??
ShopSelector(
configuration: widget.configuration,
shops: widget.shops,
onTap: (shop) {
widget.configuration.shoppingService.shopService
.selectShop(shop);
},
),
// selected categories
widget.configuration.selectedCategoryBuilder?.call(
widget.configuration,
) ??
SelectedCategories(
configuration: widget.configuration,
),
// products
_ShopContents(
configuration: widget.configuration,
),
],
),
),
// button
Align(
alignment: Alignment.bottomCenter,
child: widget.configuration.shoppingCartButtonBuilder != null
? widget.configuration.shoppingCartButtonBuilder!(
context,
widget.configuration,
)
: DefaultShoppingCartButton(
configuration: widget.configuration,
),
),
],
);
}
class _ShopContents extends StatefulWidget {
const _ShopContents({
required this.configuration,
});
final ProductPageConfiguration configuration;
@override
State<_ShopContents> createState() => _ShopContentsState();
}
class _ShopContentsState extends State<_ShopContents> {
@override
void initState() {
widget.configuration.shoppingService.shopService.addListener(_listen);
super.initState();
}
@override
void dispose() {
widget.configuration.shoppingService.shopService.removeListener(_listen);
super.dispose();
}
void _listen() {
setState(() {});
}
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Padding(
padding: EdgeInsets.symmetric(
horizontal: widget.configuration.pagePadding.horizontal,
),
child: FutureBuilder(
// ignore: discarded_futures
future: widget.configuration.getProducts(
widget.configuration.shoppingService.shopService.selectedShop!,
),
builder: (context, snapshot) {
List<Product> productPageContent;
if (snapshot.connectionState == ConnectionState.waiting) {
return SizedBox(
height: MediaQuery.of(context).size.height * 0.7,
child: const Center(
child: CircularProgressIndicator.adaptive(),
),
);
}
if (snapshot.hasError) {
if (widget.configuration.errorBuilder != null) {
return widget.configuration.errorBuilder!(
context,
snapshot.error,
);
} else {
return DefaultError(error: snapshot.error);
}
}
productPageContent =
widget.configuration.shoppingService.productService.products;
if (productPageContent.isEmpty) {
return widget.configuration.noContentBuilder?.call(context) ??
const DefaultNoContent();
}
var discountedproducts = productPageContent
.where((product) => product.hasDiscount)
.toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Discounted product
if (discountedproducts.isNotEmpty) ...[
widget.configuration.discountBuilder?.call(
context,
widget.configuration,
discountedproducts,
) ??
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: WeeklyDiscount(
configuration: widget.configuration,
product: discountedproducts.first,
),
),
],
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Text(
widget.configuration.translations.categoryItemListTitle,
style: theme.textTheme.titleLarge,
textAlign: TextAlign.start,
),
),
widget.configuration.categoryListBuilder?.call(
context,
widget.configuration,
productPageContent,
) ??
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Column(
children: [
// Products
getCategoryList(
context,
widget.configuration,
widget.configuration.shoppingService.productService
.products,
),
// Bottom padding so the last product is not cut off
// by the to shopping cart button.
const SizedBox(height: 48),
],
),
),
],
);
},
),
);
}
}

View file

@ -1,31 +1,18 @@
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";
/// A function that is called when a product is added to the cart.
ProductPageProduct onAddToCartWrapper(
ProductPageConfiguration configuration,
ShoppingCartNotifier shoppingCartNotifier,
ProductPageProduct product,
) {
shoppingCartNotifier.productsChanged();
configuration.onAddToCart(product);
return product;
}
import "package:flutter_product_page/src/widgets/defaults/default_product_item.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Generates a [CategoryList] from a list of [Product]s and a
/// [ProductPageConfiguration].
CategoryList getCategoryList(
Widget getCategoryList(
BuildContext context,
ProductPageConfiguration configuration,
ShoppingCartNotifier shoppingCartNotifier,
List<ProductPageProduct> products,
List<Product> products,
) {
var categorizedProducts = <String, List<ProductPageProduct>>{};
var theme = Theme.of(context);
var categorizedProducts = <String, List<Product>>{};
for (var product in products) {
if (!categorizedProducts.containsKey(product.category)) {
categorizedProducts[product.category] = [];
@ -38,19 +25,15 @@ CategoryList getCategoryList(
categorizedProducts.forEach((categoryName, productList) {
var productWidgets = productList
.map(
(product) => configuration.productBuilder != null
? configuration.productBuilder!(context, product)
: ProductItem(
product: product,
onProductDetail: configuration.onProductDetail,
onAddToCart: (ProductPageProduct product) =>
onAddToCartWrapper(
configuration,
shoppingCartNotifier,
product,
),
localizations: configuration.localizations,
),
(product) =>
configuration.productBuilder
?.call(context, product, configuration) ??
DefaultProductItem(
product: product,
onAddToCart: configuration.onAddToCart,
onProductDetail: configuration.onProductDetail!,
translations: configuration.translations,
),
)
.toList();
var category = Category(
@ -59,15 +42,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),
],
],
);
}

View file

@ -1,21 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/src/models/product_page_shop.dart";
/// A service that provides the currently selected shop.
class SelectedShopService extends ChangeNotifier {
/// Creates a [SelectedShopService].
SelectedShopService();
ProductPageShop? _selectedShop;
/// Updates the selected shop.
void selectShop(ProductPageShop shop) {
if (_selectedShop == shop) return;
_selectedShop = shop;
notifyListeners();
}
/// The currently selected shop.
ProductPageShop? get selectedShop => _selectedShop;
}

View file

@ -1,10 +0,0 @@
import "package:flutter/material.dart";
/// Class that notifies listeners when the products in the shopping cart have
/// changed.
class ShoppingCartNotifier extends ChangeNotifier {
/// Notifies listeners that the products in the shopping cart have changed.
void productsChanged() {
notifyListeners();
}
}

View file

@ -1,63 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_product_page/src/services/selected_shop_service.dart";
import "package:flutter_product_page/src/ui/widgets/horizontal_list_items.dart";
import "package:flutter_product_page/src/ui/widgets/spaced_wrap.dart";
/// Shop selector widget that displays a list to navigate between shops.
class ShopSelector extends StatelessWidget {
/// Constructor for the shop selector.
const ShopSelector({
required this.configuration,
required this.selectedShopService,
required this.shops,
required this.onTap,
this.paddingBetweenButtons = 4,
this.paddingOnButtons = 8,
super.key,
});
/// Configuration for the product page.
final ProductPageConfiguration configuration;
/// Service for the selected shop.
final SelectedShopService selectedShopService;
/// List of shops.
final List<ProductPageShop> shops;
/// Callback when a shop is tapped.
final Function(ProductPageShop shop) onTap;
/// Padding between the buttons.
final double paddingBetweenButtons;
/// Padding on the buttons.
final double paddingOnButtons;
@override
Widget build(BuildContext context) {
if (shops.length == 1) {
return const SizedBox.shrink();
}
if (configuration.shopSelectorStyle == ShopSelectorStyle.spacedWrap) {
return SpacedWrap(
shops: shops,
selectedItem: selectedShopService.selectedShop!.id,
onTap: onTap,
width: MediaQuery.of(context).size.width - (16 * 2),
paddingBetweenButtons: paddingBetweenButtons,
paddingOnButtons: paddingOnButtons,
);
}
return HorizontalListItems(
shops: shops,
selectedItem: selectedShopService.selectedShop!.id,
onTap: onTap,
paddingBetweenButtons: paddingBetweenButtons,
paddingOnButtons: paddingOnButtons,
);
}
}

View file

@ -1,288 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_product_page/src/services/category_service.dart";
import "package:flutter_product_page/src/services/selected_shop_service.dart";
import "package:flutter_product_page/src/services/shopping_cart_notifier.dart";
import "package:flutter_product_page/src/ui/components/shop_selector.dart";
import "package:flutter_product_page/src/ui/components/weekly_discount.dart";
/// A page that displays products.
class ProductPage extends StatelessWidget {
/// Constructor for the product page.
ProductPage({
required this.configuration,
this.initialBuildShopId,
super.key,
});
/// Configuration for the product page.
final ProductPageConfiguration configuration;
/// An optional initial shop ID to select. This overrides the initialShopId
/// from the configuration.
final String? initialBuildShopId;
late final SelectedShopService _selectedShopService = SelectedShopService();
late final ShoppingCartNotifier _shoppingCartNotifier =
ShoppingCartNotifier();
@override
Widget build(BuildContext context) => Padding(
padding: configuration.pagePadding,
child: FutureBuilder(
future: configuration.shops,
builder: (BuildContext context, AsyncSnapshot data) {
if (data.connectionState == ConnectionState.waiting) {
return const Align(
alignment: Alignment.center,
child: CircularProgressIndicator.adaptive(),
);
}
if (data.hasError) {
return configuration.errorBuilder!(
context,
data.error,
data.stackTrace,
);
}
List<ProductPageShop>? shops = data.data;
if (shops == null || shops.isEmpty) {
return configuration.errorBuilder!(context, null, null);
}
if (initialBuildShopId != null) {
ProductPageShop? initialShop;
for (var shop in shops) {
if (shop.id == initialBuildShopId) {
initialShop = shop;
break;
}
}
_selectedShopService.selectShop(initialShop ?? shops.first);
} else if (configuration.initialShopId != null) {
ProductPageShop? initialShop;
for (var shop in shops) {
if (shop.id == configuration.initialShopId) {
initialShop = shop;
break;
}
}
_selectedShopService.selectShop(initialShop ?? shops.first);
} else {
_selectedShopService.selectShop(shops.first);
}
return ListenableBuilder(
listenable: _selectedShopService,
builder: (BuildContext context, Widget? _) {
configuration.onShopSelectionChange?.call(
_selectedShopService.selectedShop!,
);
return _ProductPage(
configuration: configuration,
selectedShopService: _selectedShopService,
shoppingCartNotifier: _shoppingCartNotifier,
shops: shops,
);
},
);
},
),
);
}
class _ProductPage extends StatelessWidget {
const _ProductPage({
required this.configuration,
required this.selectedShopService,
required this.shoppingCartNotifier,
required this.shops,
});
final ProductPageConfiguration configuration;
final SelectedShopService selectedShopService;
final ShoppingCartNotifier shoppingCartNotifier;
final List<ProductPageShop> shops;
void _onTapChangeShop(ProductPageShop shop) {
selectedShopService.selectShop(shop);
}
@override
Widget build(BuildContext context) {
var pageContent = SingleChildScrollView(
child: Column(
children: [
ShopSelector(
configuration: configuration,
selectedShopService: selectedShopService,
shops: shops,
onTap: _onTapChangeShop,
),
_ShopContents(
configuration: configuration,
selectedShopService: selectedShopService,
shoppingCartNotifier: shoppingCartNotifier,
),
],
),
);
return Stack(
children: [
pageContent,
Align(
alignment: Alignment.bottomCenter,
child: configuration.navigateToShoppingCartBuilder != 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,
),
),
),
),
);
}
}
class _ShopContents extends StatelessWidget {
const _ShopContents({
required this.configuration,
required this.selectedShopService,
required this.shoppingCartNotifier,
});
final ProductPageConfiguration configuration;
final SelectedShopService selectedShopService;
final ShoppingCartNotifier shoppingCartNotifier;
@override
Widget build(BuildContext context) => Padding(
padding: EdgeInsets.symmetric(
horizontal: configuration.pagePadding.horizontal,
),
child: FutureBuilder(
// ignore: discarded_futures
future: configuration.getProducts(
selectedShopService.selectedShop!,
),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Align(
alignment: Alignment.center,
child: CircularProgressIndicator.adaptive(),
);
}
if (snapshot.hasError) {
return configuration.errorBuilder!(
context,
snapshot.error,
snapshot.stackTrace,
);
}
var productPageContent = snapshot.data;
if (productPageContent == null ||
productPageContent.products.isEmpty) {
return configuration.noContentBuilder!(context);
}
var productList = Padding(
padding: const EdgeInsets.fromLTRB(16, 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),
],
),
);
return Column(
children: [
// Discounted product
if (productPageContent.discountedProduct != null) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: WeeklyDiscount(
configuration: configuration,
product: productPageContent.discountedProduct!,
),
),
],
productList,
],
);
},
),
);
}

View file

@ -1,36 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/src/configuration/product_page_configuration.dart";
import "package:flutter_product_page/src/ui/product_page.dart";
/// A screen that displays a product page. This screen contains a Scaffold,
/// in which the body is a SafeArea that contains a ProductPage widget.
///
/// If you do not wish to create a Scaffold you can use the
/// [ProductPage] widget directly.
class ProductPageScreen extends StatelessWidget {
/// Constructor for the product page screen.
const ProductPageScreen({
required this.configuration,
this.initialBuildShopId,
super.key,
});
/// Configuration for the product page.
final ProductPageConfiguration configuration;
/// An optional initial shop ID to select. This overrides the initialShopId
/// from the configuration.
final String? initialBuildShopId;
@override
Widget build(BuildContext context) => Scaffold(
body: SafeArea(
child: ProductPage(
configuration: configuration,
initialBuildShopId: initialBuildShopId,
),
),
appBar: configuration.appBar,
bottomNavigationBar: configuration.bottomNavigationBar,
);
}

View file

@ -1,73 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
/// Horizontal list of items.
class HorizontalListItems extends StatelessWidget {
/// Constructor for the horizontal list of items.
const HorizontalListItems({
required this.shops,
required this.selectedItem,
required this.onTap,
this.paddingBetweenButtons = 2.0,
this.paddingOnButtons = 4,
super.key,
});
/// List of items.
final List<ProductPageShop> shops;
/// Selected item.
final String selectedItem;
/// Padding between the buttons.
final double paddingBetweenButtons;
/// Padding on the buttons.
final double paddingOnButtons;
/// Callback when an item is tapped.
final Function(ProductPageShop shop) onTap;
@override
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,
),
),
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(),
),
);
}
}

View file

@ -1,69 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.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 ProductPageProduct product;
/// Configuration for the product page.
final ProductPageConfiguration configuration;
@override
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,
],
),
),
);
}
}

View file

@ -1,150 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
/// SpacedWrap is a widget that wraps a list of items that are spaced out and
/// fill the available width.
class SpacedWrap extends StatelessWidget {
/// Creates a [SpacedWrap].
const SpacedWrap({
required this.shops,
required this.onTap,
required this.width,
this.paddingBetweenButtons = 2.0,
this.paddingOnButtons = 4.0,
this.selectedItem = "",
super.key,
});
/// List of items.
final List<ProductPageShop> shops;
/// Selected item.
final String selectedItem;
/// Width of the widget.
final double width;
/// Padding between the buttons.
final double paddingBetweenButtons;
/// Padding on the buttons.
final double paddingOnButtons;
/// Callback when an item is tapped.
final Function(ProductPageShop shop) onTap;
Row _buildRow(
BuildContext context,
List<int> currentRow,
double availableRowLength,
) {
var theme = Theme.of(context);
var row = <Widget>[];
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,
),
),
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<Row> _buildButtonRows(BuildContext context) {
var theme = Theme.of(context);
var rows = <Row>[];
var currentRow = <int>[];
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 = <int>[];
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,
),
);
}

View file

@ -0,0 +1,45 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_product_page/src/category_selection_screen.dart";
/// Default appbar for the product page.
class DefaultAppbar extends StatelessWidget implements PreferredSizeWidget {
/// Constructor for the default appbar for the product page.
const DefaultAppbar({
required this.configuration,
super.key,
});
/// Configuration for the product page.
final ProductPageConfiguration configuration;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return AppBar(
leading: IconButton(onPressed: () {}, icon: const Icon(Icons.person)),
actions: [
IconButton(
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => CategorySelectionScreen(
configuration: configuration,
),
),
);
},
icon: const Icon(Icons.filter_alt),
),
],
title: Text(
configuration.translations.appBarTitle,
style: theme.textTheme.headlineLarge,
),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

View file

@ -0,0 +1,24 @@
import "package:flutter/material.dart";
/// Default error widget.
class DefaultError extends StatelessWidget {
/// Constructor for the default error widget.
const DefaultError({
super.key,
this.error,
});
/// Error that occurred.
final Object? error;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Center(
child: Text(
"Error: $error",
style: theme.textTheme.titleLarge,
),
);
}
}

View file

@ -0,0 +1,18 @@
import "package:flutter/material.dart";
/// Default no content widget.
class DefaultNoContent extends StatelessWidget {
/// Constructor for the default no content widget.
const DefaultNoContent({super.key});
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Center(
child: Text(
"No content",
style: theme.textTheme.titleLarge,
),
);
}
}

View file

@ -1,31 +1,35 @@
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
import "package:skeletonizer/skeletonizer.dart";
/// Product item widget.
class ProductItem extends StatelessWidget {
class DefaultProductItem extends StatelessWidget {
/// Constructor for the product item widget.
const ProductItem({
const DefaultProductItem({
required this.product,
required this.onProductDetail,
required this.onAddToCart,
required this.localizations,
required this.translations,
super.key,
});
/// 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,
String closeText,
) 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;
final ProductPageTranslations translations;
/// Size of the product image.
static const double imageSize = 44;
@ -47,7 +51,7 @@ class ProductItem extends StatelessWidget {
fit: BoxFit.cover,
placeholder: (context, url) => loadingImageSkeleton,
errorWidget: (context, url, error) => Tooltip(
message: localizations.failedToLoadImageExplenation,
message: translations.failedToLoadImageExplenation,
child: Container(
width: 48,
height: 48,
@ -75,8 +79,15 @@ class ProductItem extends StatelessWidget {
var productInformationIcon = Padding(
padding: const EdgeInsets.only(left: 4),
child: IconButton(
onPressed: () => onProductDetail(context, product),
icon: const Icon(Icons.info_outline),
onPressed: () => onProductDetail(
context,
product,
translations.close,
),
icon: Icon(
Icons.info_outline,
color: theme.colorScheme.primary,
),
),
);
@ -84,10 +95,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 +121,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 +160,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),
),
),
),
),
),
);

View file

@ -0,0 +1,71 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
/// Default shopping cart button for the product page.
class DefaultShoppingCartButton extends StatefulWidget {
/// Constructor for the default shopping cart button for the product page.
const DefaultShoppingCartButton({
required this.configuration,
super.key,
});
/// Configuration for the product page.
final ProductPageConfiguration configuration;
@override
State<DefaultShoppingCartButton> createState() =>
_DefaultShoppingCartButtonState();
}
class _DefaultShoppingCartButtonState extends State<DefaultShoppingCartButton> {
@override
void initState() {
super.initState();
widget.configuration.shoppingService.shoppingCartService
.addListener(_listen);
}
@override
void dispose() {
widget.configuration.shoppingService.shoppingCartService
.removeListener(_listen);
super.dispose();
}
void _listen() {
setState(() {});
}
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: widget.configuration.shoppingService.shoppingCartService
.products.isNotEmpty
? widget.configuration.onNavigateToShoppingCart
: null,
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12,
),
child: Text(
widget.configuration.translations.navigateToShoppingCart,
style: theme.textTheme.displayLarge,
),
),
),
),
);
}
}

View file

@ -0,0 +1,72 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
/// Selected categories.
class SelectedCategories extends StatefulWidget {
/// Constructor for the selected categories.
const SelectedCategories({
required this.configuration,
super.key,
});
/// Configuration for the product page.
final ProductPageConfiguration configuration;
@override
State<SelectedCategories> createState() => _SelectedCategoriesState();
}
class _SelectedCategoriesState extends State<SelectedCategories> {
@override
void initState() {
widget.configuration.shoppingService.productService.addListener(_listen);
super.initState();
}
@override
void dispose() {
widget.configuration.shoppingService.productService.removeListener(_listen);
super.dispose();
}
void _listen() {
setState(() {});
}
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.only(left: 4),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (var category in widget.configuration.shoppingService
.productService.selectedCategories) ...[
Padding(
padding: const EdgeInsets.only(right: 8),
child: Chip(
backgroundColor: theme.colorScheme.primary,
deleteIcon: const Icon(
Icons.close,
color: Colors.white,
),
onDeleted: () {
widget.configuration.shoppingService.productService
.selectCategory(category);
},
label: Text(
category,
style: theme.textTheme.bodyMedium
?.copyWith(color: Colors.white),
),
),
),
],
],
),
),
);
}
}

View file

@ -0,0 +1,78 @@
import "package:flutter/material.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Horizontal list of items.
class HorizontalListItems extends StatelessWidget {
/// Constructor for the horizontal list of items.
const HorizontalListItems({
required this.shops,
required this.selectedItem,
required this.onTap,
this.paddingBetweenButtons = 2.0,
this.paddingOnButtons = 6,
super.key,
});
/// List of items.
final List<Shop> shops;
/// Selected item.
final String selectedItem;
/// Padding between the buttons.
final double paddingBetweenButtons;
/// Padding on the buttons.
final double paddingOnButtons;
/// Callback when an item is tapped.
final Function(Shop shop) onTap;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.only(
top: 4,
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: shops
.map(
(shop) => Padding(
padding: EdgeInsets.only(right: paddingBetweenButtons),
child: InkWell(
onTap: () => onTap(shop),
child: Container(
decoration: BoxDecoration(
color: shop.id == selectedItem
? theme.colorScheme.primary
: Colors.white,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: theme.colorScheme.primary,
width: 1,
),
),
padding: EdgeInsets.all(paddingOnButtons),
child: Text(
shop.name,
style: shop.id == selectedItem
? theme.textTheme.bodyMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
)
: theme.textTheme.bodyMedium,
),
),
),
),
)
.toList(),
),
),
);
}
}

View file

@ -0,0 +1,65 @@
import "package:flutter/material.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// A popup that displays the product item.
class ProductItemPopup extends StatelessWidget {
/// Constructor for the product item popup.
const ProductItemPopup({
required this.product,
required this.closeText,
super.key,
});
/// The product to display.
final Product product;
/// Configuration for the product page.
final String closeText;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(32),
child: SizedBox(
width: double.infinity,
child: Column(
children: [
Text(
product.description,
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
Padding(
padding: const EdgeInsets.only(top: 20, left: 40, right: 40),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () => Navigator.of(context).pop(),
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Text(
closeText,
style: theme.textTheme.displayLarge,
),
),
),
),
),
],
),
),
),
);
}
}

View file

@ -0,0 +1,85 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_product_page/src/widgets/horizontal_list_items.dart";
import "package:flutter_product_page/src/widgets/spaced_wrap.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Shop selector widget that displays a list to navigate between shops.
class ShopSelector extends StatefulWidget {
/// Constructor for the shop selector.
const ShopSelector({
required this.configuration,
required this.shops,
required this.onTap,
this.paddingBetweenButtons = 4,
this.paddingOnButtons = 8,
super.key,
});
/// Configuration for the product page.
final ProductPageConfiguration configuration;
/// Service for the selected shop.
/// List of shops.
final List<Shop> shops;
/// Callback when a shop is tapped.
final Function(Shop shop) onTap;
/// Padding between the buttons.
final double paddingBetweenButtons;
/// Padding on the buttons.
final double paddingOnButtons;
@override
State<ShopSelector> createState() => _ShopSelectorState();
}
class _ShopSelectorState extends State<ShopSelector> {
@override
void initState() {
widget.configuration.shoppingService.shopService.addListener(_listen);
super.initState();
}
@override
void dispose() {
widget.configuration.shoppingService.shopService.removeListener(_listen);
super.dispose();
}
void _listen() {
setState(() {});
}
@override
Widget build(BuildContext context) {
if (widget.shops.length == 1) {
return const SizedBox.shrink();
}
if (widget.configuration.shopSelectorStyle ==
ShopSelectorStyle.spacedWrap) {
return SpacedWrap(
shops: widget.shops,
selectedItem:
widget.configuration.shoppingService.shopService.selectedShop!.id,
onTap: widget.onTap,
width: MediaQuery.of(context).size.width - (16 * 2),
paddingBetweenButtons: widget.paddingBetweenButtons,
paddingOnButtons: widget.paddingOnButtons,
);
}
return HorizontalListItems(
shops: widget.shops,
selectedItem:
widget.configuration.shoppingService.shopService.selectedShop!.id,
onTap: widget.onTap,
paddingBetweenButtons: widget.paddingBetweenButtons,
paddingOnButtons: widget.paddingOnButtons,
);
}
}

View file

@ -0,0 +1,76 @@
import "package:flutter/material.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// SpacedWrap is a widget that wraps a list of items that are spaced out and
/// fill the available width.
class SpacedWrap extends StatelessWidget {
/// Creates a [SpacedWrap].
const SpacedWrap({
required this.shops,
required this.onTap,
required this.width,
this.paddingBetweenButtons = 2.0,
this.paddingOnButtons = 4.0,
this.selectedItem = "",
super.key,
});
/// List of items.
final List<Shop> shops;
/// Selected item.
final String selectedItem;
/// Width of the widget.
final double width;
/// Padding between the buttons.
final double paddingBetweenButtons;
/// Padding on the buttons.
final double paddingOnButtons;
/// Callback when an item is tapped.
final Function(Shop shop) onTap;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Wrap(
alignment: WrapAlignment.center,
spacing: 4,
children: [
for (var shop in shops) ...[
Padding(
padding: EdgeInsets.only(top: paddingBetweenButtons),
child: InkWell(
onTap: () => onTap(shop),
child: DecoratedBox(
decoration: BoxDecoration(
color: shop.id == selectedItem
? Theme.of(context).colorScheme.primary
: Colors.white,
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: Theme.of(context).colorScheme.primary,
width: 1,
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
shop.name,
style: shop.id == selectedItem
? theme.textTheme.titleMedium
?.copyWith(color: Colors.white)
: theme.textTheme.bodyMedium,
),
),
),
),
),
],
],
);
}
}

View file

@ -1,6 +1,7 @@
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// A widget that displays a weekly discount.
class WeeklyDiscount extends StatelessWidget {
@ -15,10 +16,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) {
@ -27,10 +28,8 @@ class WeeklyDiscount extends StatelessWidget {
var bottomText = Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
configuration.getDiscountDescription!(product),
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.primary,
),
configuration.discountDescription!(product),
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.left,
),
);
@ -52,7 +51,7 @@ class WeeklyDiscount extends StatelessWidget {
Icons.error_outline_rounded,
color: Colors.red,
),
Text(configuration.localizations.failedToLoadImageExplenation),
Text(configuration.translations.failedToLoadImageExplenation),
],
),
),
@ -73,9 +72,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 +87,8 @@ class WeeklyDiscount extends StatelessWidget {
horizontal: 16,
),
child: Text(
configuration.localizations.discountTitle.toUpperCase(),
style: theme.textTheme.titleMedium?.copyWith(
color: theme.colorScheme.onPrimary,
),
configuration.translations.discountTitle,
style: theme.textTheme.headlineSmall,
textAlign: TextAlign.left,
),
),
@ -100,7 +97,6 @@ class WeeklyDiscount extends StatelessWidget {
var boxDecoration = BoxDecoration(
border: Border.all(
color: theme.primaryColor,
width: 1.0,
),
borderRadius: BorderRadius.circular(4.0),

View file

@ -1,10 +1,10 @@
name: flutter_product_page
description: "A Flutter module for the product page"
publish_to: 'none'
version: 1.0.0
publish_to: "none"
version: 2.0.0
environment:
sdk: '>=3.3.4 <4.0.0'
sdk: ">=3.3.4 <4.0.0"
dependencies:
flutter:
@ -15,6 +15,13 @@ dependencies:
git:
url: https://github.com/Iconica-Development/flutter_nested_categories
ref: 0.0.1
flutter_shopping_interface:
git:
url: https://github.com/Iconica-Development/flutter_shopping
path: packages/flutter_shopping_interface
ref: 2.0.0
collection: ^1.18.0
provider: ^6.1.2
dev_dependencies:
flutter_test:
@ -26,13 +33,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

View file

@ -1,53 +0,0 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
.metadata
pubspec.lock
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Platforms
android/
ios/
linux/
macos/
web/
windows/

View file

@ -1,16 +0,0 @@
# example
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View file

@ -1,7 +0,0 @@
include: package:flutter_iconica_analysis/analysis_options.yaml
analyzer:
exclude:
linter:
rules:

View file

@ -1,22 +0,0 @@
import "package:example/src/routes.dart";
import "package:example/src/utils/theme.dart";
import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends HookConsumerWidget {
const MyApp({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) => MaterialApp.router(
debugShowCheckedModeBanner: false,
restorationScopeId: "app",
theme: getTheme(),
routerConfig: ref.read(routerProvider),
);
}

View file

@ -1,193 +0,0 @@
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";
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:go_router/go_router.dart";
// (REQUIRED): Create your own instance of the ProductService.
final ProductService<MyProduct> productService = ProductService([]);
FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
FlutterShoppingConfiguration(
// (REQUIRED): Shop builder configuration
shopBuilder: (
BuildContext context,
String? initialBuildShopId,
String? streetName,
) =>
ProductPageScreen(
configuration: ProductPageConfiguration(
// (REQUIRED): List of shops that should be displayed
// If there is only one, make a list with just one shop.
shops: Future.value(getShops()),
// (REQUIRED): Function to add a product to the cart
onAddToCart: (ProductPageProduct product) =>
productService.addProduct(product as MyProduct),
// (REQUIRED): Function to get the products for a shop
getProducts: (ProductPageShop shop) =>
Future<ProductPageContent>.value(
getShopContent(shop.id),
),
// (REQUIRED): Function to navigate to the shopping cart
onNavigateToShoppingCart: () => onCompleteProductPage(context),
// (RECOMMENDED): Function to get the number of products in the
// shopping cart. This is used to display the number of products
// in the shopping cart on the product page.
getProductsInShoppingCart: productService.countProducts,
// (RECOMMENDED) Function that returns the description for a
// product that is on sale.
getDiscountDescription: (ProductPageProduct product) =>
"""${product.name} for just \$${product.discountPrice?.toStringAsFixed(2)}""",
// (RECOMMENDED) Function that is fired when the shop selection
// changes. You could use this to clear your shopping cart or to
// change the products so they belong to the correct shop again.
onShopSelectionChange: (ProductPageShop shop) =>
productService.clear(),
// (RECOMMENDED) The shop that is initially selected.
// Must be one of the shops in the [shops] list.
initialShopId: getShops().first.id,
// (RECOMMENDED) Localizations for the product page.
localizations: const ProductPageLocalization(),
// (OPTIONAL) Appbar
appBar: AppBar(
title: const Text("Shop"),
leading: IconButton(
icon: const Icon(
Icons.arrow_back,
color: Colors.white,
),
onPressed: () {
context.go(homePage);
},
),
),
),
// (OPTIONAL): Initial build shop id that overrides the initialShop
initialBuildShopId: initialBuildShopId,
),
// (REQUIRED): Shopping cart builder configuration
shoppingCartBuilder: (BuildContext context) => ShoppingCartScreen(
configuration: ShoppingCartConfig(
// (REQUIRED) product service instance:
productService: productService,
// (REQUIRED) product item builder:
productItemBuilder: (context, locale, product) => ListTile(
title: Text(product.name),
subtitle: Text(product.price.toStringAsFixed(2)),
leading: Image.network(
product.imageUrl,
errorBuilder: (context, error, stackTrace) => const Tooltip(
message: "Error loading image",
child: Icon(
Icons.error,
color: Colors.red,
),
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () => productService.removeOneProduct(product),
),
Text("${product.quantity}"),
IconButton(
icon: const Icon(Icons.add),
onPressed: () => productService.addProduct(product),
),
],
),
),
// (OPTIONAL/REQUIRED) on confirm order callback:
// Either use this callback or the placeOrderButtonBuilder.
onConfirmOrder: (products) => onCompleteShoppingCart(context),
// (RECOMMENDED) localizations:
localizations: const ShoppingCartLocalizations(),
// (OPTIONAL) title above product list:
title: "Products",
/// (OPTIONAL) no content builder for when there are no products
/// in the shopping cart.
noContentBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 128),
child: Column(
children: [
Icon(
Icons.warning,
),
SizedBox(
height: 16,
),
Text(
"Geen producten in winkelmandje",
),
],
),
),
),
// (OPTIONAL) custom appbar:
appBar: AppBar(
title: const Text("Shopping Cart"),
leading: IconButton(
icon: const Icon(
Icons.arrow_back,
color: Colors.white,
),
onPressed: () {
context.go(FlutterShoppingPathRoutes.shop);
},
),
),
),
),
// (REQUIRED): Configuration on what to do when the user story is
// completed.
onCompleteUserStory: (BuildContext context) {
context.go(homePage);
},
// (RECOMMENDED) Handle processing of the order details. This function
// should return true if the order was processed successfully, otherwise
// false.
//
// If this function is not provided, it is assumed that the order is
// always processed successfully.
//
// Example use cases that could be implemented here:
// - Sending and storing the order on a server,
// - Processing payment (if the user decides to pay upfront).
// - And many more...
onCompleteOrderDetails:
(BuildContext context, OrderResult orderDetails) async {
if (orderDetails.order["payment_option"] == "Pay now") {
// Make the user pay upfront.
}
// If all went well, we can store the order in the database.
// Make sure to register whether or not the order was paid.
storeOrderInDatabase(productService.products, orderDetails);
return true;
},
);

View file

@ -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;
}

View file

@ -1,8 +0,0 @@
import "package:flutter_shopping/flutter_shopping.dart";
class MyShop extends ProductPageShop {
const MyShop({
required super.id,
required super.name,
});
}

View file

@ -1,31 +0,0 @@
import "package:example/src/configuration/configuration.dart";
import "package:example/src/ui/homepage.dart";
import "package:example/src/utils/go_router.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:go_router/go_router.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
const String homePage = "/";
final routerProvider = Provider<GoRouter>(
(ref) => GoRouter(
initialLocation: homePage,
routes: [
// Flutter Shopping Story Routes
...getShoppingStoryRoutes(
configuration: getFlutterShoppingConfiguration(),
),
// Home Route
GoRoute(
name: "home",
path: homePage,
pageBuilder: (context, state) => buildScreenWithFadeTransition(
context: context,
state: state,
child: const Homepage(),
),
),
],
),
);

View file

@ -1,7 +0,0 @@
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<MyProduct> products, OrderResult result) {
return;
}

View file

@ -1,47 +0,0 @@
import "package:example/src/models/my_product.dart";
import "package:example/src/models/my_shop.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// This function should have your own implementation. Generally this would
/// contain some API call to fetch the list of shops.
List<MyShop> getShops() => <MyShop>[
const MyShop(id: "1", name: "Shop 1"),
const MyShop(id: "2", name: "Shop 2"),
const MyShop(id: "3", name: "Shop 3"),
];
ProductPageContent getShopContent(String shopId) {
var products = getProducts(shopId);
return ProductPageContent(
discountedProduct: products.first,
products: products,
);
}
/// This function should have your own implementation. Generally this would
/// contain some API call to fetch the list of products for a shop.
List<MyProduct> getProducts(String shopId) => <MyProduct>[
MyProduct(
id: "1",
name: "White bread",
price: 2.99,
category: "Loaves",
imageUrl: "https://via.placeholder.com/150",
hasDiscount: true,
discountPrice: 1.99,
),
MyProduct(
id: "2",
name: "Brown bread",
price: 2.99,
category: "Loaves",
imageUrl: "https://via.placeholder.com/150",
),
MyProduct(
id: "3",
name: "Cheese sandwich",
price: 1.99,
category: "Sandwiches",
imageUrl: "https://via.placeholder.com/150",
),
];

View file

@ -1,20 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:go_router/go_router.dart";
class Homepage extends StatelessWidget {
const Homepage({super.key});
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: Badge(
label: const Text("1"),
child: IconButton(
icon: const Icon(Icons.shopping_cart_outlined, size: 50),
onPressed: () => context.go(FlutterShoppingPathRoutes.shop),
),
),
),
);
}

View file

@ -1,26 +0,0 @@
import "package:flutter/material.dart";
import "package:go_router/go_router.dart";
CustomTransitionPage buildScreenWithFadeTransition<T>({
required BuildContext context,
required GoRouterState state,
required Widget child,
}) =>
CustomTransitionPage<T>(
key: state.pageKey,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
);
CustomTransitionPage buildScreenWithoutTransition<T>({
required BuildContext context,
required GoRouterState state,
required Widget child,
}) =>
CustomTransitionPage<T>(
key: state.pageKey,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
child,
);

View file

@ -1,32 +0,0 @@
import "package:flutter/material.dart";
ThemeData getTheme() => ThemeData(
scaffoldBackgroundColor: const Color.fromRGBO(250, 249, 246, 1),
textTheme: const TextTheme(
labelMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Colors.black,
),
titleMedium: TextStyle(
fontSize: 16,
color: Color.fromRGBO(60, 60, 59, 1),
fontWeight: FontWeight.w700,
),
),
inputDecorationTheme: const InputDecorationTheme(
fillColor: Colors.white,
),
colorScheme: const ColorScheme.light(
primary: Color.fromRGBO(64, 87, 122, 1),
secondary: Colors.white,
surface: Color.fromRGBO(250, 249, 246, 1),
),
appBarTheme: const AppBarTheme(
backgroundColor: Color.fromRGBO(64, 87, 122, 1),
titleTextStyle: TextStyle(
fontSize: 28,
color: Colors.white,
),
),
);

View file

@ -1,41 +0,0 @@
name: example
description: Demonstrates how to use the flutter_shopping package."
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0
environment:
sdk: '>=3.3.4 <4.0.0'
dependencies:
flutter:
sdk: flutter
flutter_hooks: ^0.20.0
hooks_riverpod: ^2.1.1
go_router: 12.1.3
# Iconica packages
## Userstories
flutter_shopping:
path: ../
dev_dependencies:
flutter_test:
sdk: flutter
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
flutter:
uses-material-design: true
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic

View file

@ -1,53 +0,0 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
.metadata
pubspec.lock
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Platforms
/android/
/ios/
/linux/
/macos/
/web/
/windows/

View file

@ -1,16 +0,0 @@
# amazon
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View file

@ -1,28 +0,0 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

View file

@ -1,22 +0,0 @@
import "package:amazon/src/routes.dart";
import "package:amazon/src/utils/theme.dart";
import "package:flutter/material.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
void main() {
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends HookConsumerWidget {
const MyApp({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) => MaterialApp.router(
debugShowCheckedModeBanner: false,
restorationScopeId: "app",
theme: getTheme(),
routerConfig: ref.read(routerProvider),
);
}

View file

@ -1,359 +0,0 @@
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";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:go_router/go_router.dart";
// (REQUIRED): Create your own instance of the ProductService.
final ProductService<MyProduct> productService = ProductService([]);
FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
FlutterShoppingConfiguration(
// (REQUIRED): Shop builder configuration
shopBuilder: (
BuildContext context,
String? initialBuildShopId,
String? streetName,
) {
var theme = Theme.of(context);
return ProductPageScreen(
configuration: ProductPageConfiguration(
// (REQUIRED): List of shops that should be displayed
// If there is only one, make a list with just one shop.
shops: Future.value(getCategories()),
pagePadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4),
// (REQUIRED): Function to add a product to the cart
onAddToCart: (ProductPageProduct product) =>
productService.addProduct(product as MyProduct),
// (REQUIRED): Function to get the products for a shop
getProducts: (ProductPageShop shop) =>
Future<ProductPageContent>.value(
getShopContent(shop.id),
),
// (REQUIRED): Function to navigate to the shopping cart
onNavigateToShoppingCart: () => onCompleteProductPage(context),
shopSelectorStyle: ShopSelectorStyle.row,
navigateToShoppingCartBuilder: (context) => const SizedBox.shrink(),
bottomNavigationBar: BottomNavigationBar(
fixedColor: theme.primaryColor,
unselectedItemColor: Colors.black,
type: BottomNavigationBarType.fixed,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: "Home",
),
BottomNavigationBarItem(
icon: Icon(Icons.person_2_outlined),
label: "Profile",
),
BottomNavigationBarItem(
icon: Icon(Icons.shopping_cart_outlined),
label: "Cart",
),
BottomNavigationBarItem(
icon: Icon(Icons.menu),
label: "Menu",
),
],
showSelectedLabels: false,
showUnselectedLabels: false,
onTap: (index) {
switch (index) {
case 0:
// context.go(homePage);
break;
case 1:
break;
case 2:
context.go(FlutterShoppingPathRoutes.shoppingCart);
break;
case 3:
break;
}
},
),
productBuilder: (context, product) => Card(
elevation: 0,
color: const Color.fromARGB(255, 233, 233, 233),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
child: Row(
children: [
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Image.network(
product.imageUrl,
loadingBuilder: (context, child, loadingProgress) =>
loadingProgress == null
? child
: const Center(
child: CircularProgressIndicator(),
),
errorBuilder: (context, error, stackTrace) =>
const Tooltip(
message: "Error loading image",
child: Icon(
Icons.error,
color: Colors.red,
),
),
),
),
),
Expanded(
flex: 5,
child: ColoredBox(
color: theme.scaffoldBackgroundColor,
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: theme.textTheme.titleMedium,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
Row(
children: [
Text(
"4.5",
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.blue,
),
),
const Icon(Icons.star, color: Colors.orange),
const Icon(Icons.star, color: Colors.orange),
const Icon(Icons.star, color: Colors.orange),
const Icon(Icons.star, color: Colors.orange),
const Icon(Icons.star_half,
color: Colors.orange),
Text(
"(3)",
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
),
Text(
"\$${product.price.toStringAsFixed(2)}",
style: theme.textTheme.titleMedium,
),
Text(
"Gratis bezorging door Amazon",
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
const SizedBox(height: 12),
FilledButton(
onPressed: () {
productService.addProduct(product as MyProduct);
},
child: const Text("In winkelwagen"),
),
],
),
),
),
),
],
),
),
// (RECOMMENDED) The shop that is initially selected.
// Must be one of the shops in the [shops] list.
initialShopId: getCategories().first.id,
// (RECOMMENDED) Localizations for the product page.
localizations: const ProductPageLocalization(),
noContentBuilder: (context) => Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 128),
child: Column(
children: [
const Icon(
Icons.warning,
size: 48,
),
const SizedBox(
height: 16,
),
Text(
"Geen producten gevonden",
style: theme.textTheme.titleLarge,
),
],
),
),
),
// (OPTIONAL) Appbar
appBar: AppBar(
title: const SizedBox(
height: 40,
child: SearchBar(
hintText: "Search products",
leading: Icon(
Icons.search,
color: Colors.black,
),
trailing: [
Icon(
Icons.fit_screen_outlined,
),
],
),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
context.go(homePage);
},
),
bottom: AppBar(
backgroundColor: const Color.fromRGBO(203, 237, 230, 1),
title: Row(
children: [
const Icon(Icons.location_on_outlined),
const SizedBox(width: 12),
Expanded(
child: Text(
"Bestemming: ${streetName ?? "Mark - 1234AB Doetinchem Nederland"}",
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
color: Colors.black,
),
),
),
],
),
primary: false,
),
),
),
// (OPTIONAL): Initial build shop id that overrides the initialShop
initialBuildShopId: initialBuildShopId,
);
},
// (REQUIRED): Shopping cart builder configuration
shoppingCartBuilder: (BuildContext context) => ShoppingCartScreen(
configuration: ShoppingCartConfig(
// (REQUIRED) product service instance:
productService: productService,
// (REQUIRED) product item builder:
productItemBuilder: (context, locale, product) => ListTile(
title: Text(product.name),
subtitle: Text(product.price.toStringAsFixed(2)),
leading: Image.network(
product.imageUrl,
errorBuilder: (context, error, stackTrace) => const Tooltip(
message: "Error loading image",
child: Icon(
Icons.error,
color: Colors.red,
),
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () => productService.removeOneProduct(product),
),
Text("${product.quantity}"),
IconButton(
icon: const Icon(Icons.add),
onPressed: () => productService.addProduct(product),
),
],
),
),
// (OPTIONAL/REQUIRED) on confirm order callback:
// Either use this callback or the placeOrderButtonBuilder.
onConfirmOrder: (products) => onCompleteShoppingCart(context),
// (RECOMMENDED) localizations:
localizations: const ShoppingCartLocalizations(),
/// (OPTIONAL) no content builder for when there are no products
/// in the shopping cart.
noContentBuilder: (context) => const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 128),
child: Column(
children: [
Icon(
Icons.warning,
),
SizedBox(
height: 16,
),
Text(
"Geen producten in winkelmandje",
),
],
),
),
),
// (OPTIONAL) custom appbar:
appBar: AppBar(
title: const Text("Shopping Cart"),
leading: IconButton(
icon: const Icon(
Icons.arrow_back,
color: Colors.white,
),
onPressed: () {
context.go(FlutterShoppingPathRoutes.shop);
},
),
),
),
),
// (REQUIRED): Configuration on what to do when the user story is
// completed.
onCompleteUserStory: (BuildContext context) {
context.go(homePage);
},
// (RECOMMENDED) Handle processing of the order details. This function
// should return true if the order was processed successfully, otherwise
// false.
//
// If this function is not provided, it is assumed that the order is
// always processed successfully.
//
// Example use cases that could be implemented here:
// - Sending and storing the order on a server,
// - Processing payment (if the user decides to pay upfront).
// - And many more...
// onCompleteOrderDetails:
// (BuildContext context, OrderResult orderDetails) async {
// return true;
// },
);

View file

@ -1,8 +0,0 @@
import 'package:flutter_shopping/flutter_shopping.dart';
class MyCategory extends ProductPageShop {
const MyCategory({
required super.id,
required super.name,
});
}

View file

@ -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;
}

View file

@ -1,30 +0,0 @@
import "package:amazon/src/configuration/shopping_configuration.dart";
import "package:amazon/src/ui/homepage.dart";
import "package:amazon/src/utils/go_router.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:go_router/go_router.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
const String homePage = "/";
final routerProvider = Provider<GoRouter>(
(ref) => GoRouter(
initialLocation: homePage,
routes: [
// Flutter Shopping Story Routes
...getShoppingStoryRoutes(
configuration: getFlutterShoppingConfiguration(),
),
// Home Route
GoRoute(
name: "home",
path: homePage,
pageBuilder: (context, state) => buildScreenWithFadeTransition(
context: context,
state: state,
child: const Homepage(),
),
),
],
),
);

View file

@ -1,89 +0,0 @@
import "package:amazon/src/models/my_category.dart";
import "package:amazon/src/models/my_product.dart";
import "package:flutter_shopping/flutter_shopping.dart";
Map<String, String> categories = {
"Electronics": "Electronica",
"Smart phones": "Telefoons",
"TV's": "TV's",
};
List<MyProduct> allProducts() => [
MyProduct(
id: "1",
name:
"Skar Audio Single 8\" Complete 1,200 Watt EVL Series Subwoofer Bass Package - Includes Loaded Enclosure with...",
price: 2.99,
category: categories["Electronics"]!,
imageUrl:
"https://m.media-amazon.com/images/I/710n3hnbfXL._AC_UY218_.jpg",
),
MyProduct(
id: "2",
name:
"Frameo 10.1 Inch WiFi Digital Picture Frame, 1280x800 HD IPS Touch Screen Photo Frame Electronic, 32GB Memory, Auto...",
price: 2.99,
category: categories["Electronics"]!,
imageUrl:
"https://m.media-amazon.com/images/I/61O+aorCp0L._AC_UY218_.jpg",
),
MyProduct(
id: "3",
name:
"STREBITO Electronics Precision Screwdriver Sets 142-Piece with 120 Bits Magnetic Repair Tool Kit for iPhone, MacBook,...",
price: 1.99,
category: categories["Electronics"]!,
imageUrl:
"https://m.media-amazon.com/images/I/81-C7lGtQsL._AC_UY218_.jpg",
),
MyProduct(
id: "4",
name:
"Samsung Galaxy A15 (SM-155M/DSN), 128GB 6GB RAM, Dual SIM, Factory Unlocked GSM, International Version (Wall...",
price: 1.99,
category: categories["Smart phones"]!,
imageUrl:
"https://m.media-amazon.com/images/I/51rp0nqaPoL._AC_UY218_.jpg",
),
MyProduct(
id: "5",
name:
"SAMSUNG Galaxy S24 Ultra Cell Phone, 512GB AI Smartphone, Unlocked Android, 50MP Zoom Camera, Long...",
price: 1.99,
category: categories["Smart phones"]!,
imageUrl:
"https://m.media-amazon.com/images/I/71ZoDT7a2wL._AC_UY218_.jpg",
),
];
List<MyCategory> getCategories() => <MyCategory>[
MyCategory(id: "1", name: categories["Electronics"]!),
MyCategory(id: "2", name: categories["Smart phones"]!),
MyCategory(id: "3", name: categories["TV's"]!),
const MyCategory(id: "4", name: "Monitoren"),
const MyCategory(id: "5", name: "Speakers"),
const MyCategory(id: "6", name: "Toetsenborden"),
];
ProductPageContent getShopContent(String shopId) {
var products = getProducts(shopId);
return ProductPageContent(
products: products,
);
}
List<MyProduct> getProducts(String categoryId) {
if (categoryId == "1") {
return allProducts();
} else if (categoryId == "2") {
return allProducts()
.where((product) => product.category == categories["Smart phones"]!)
.toList();
} else if (categoryId == "3") {
return allProducts()
.where((product) => product.category == categories["TV's"]!)
.toList();
} else {
return [];
}
}

View file

@ -1,20 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:go_router/go_router.dart";
class Homepage extends StatelessWidget {
const Homepage({super.key});
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: Badge(
label: const Text("1"),
child: IconButton(
icon: const Icon(Icons.shopping_cart_outlined, size: 50),
onPressed: () => context.go(FlutterShoppingPathRoutes.shop),
),
),
),
);
}

View file

@ -1,26 +0,0 @@
import "package:flutter/material.dart";
import "package:go_router/go_router.dart";
CustomTransitionPage buildScreenWithFadeTransition<T>({
required BuildContext context,
required GoRouterState state,
required Widget child,
}) =>
CustomTransitionPage<T>(
key: state.pageKey,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
);
CustomTransitionPage buildScreenWithoutTransition<T>({
required BuildContext context,
required GoRouterState state,
required Widget child,
}) =>
CustomTransitionPage<T>(
key: state.pageKey,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
child,
);

View file

@ -1,43 +0,0 @@
import "package:flutter/material.dart";
ThemeData getTheme() => ThemeData(
scaffoldBackgroundColor: const Color.fromRGBO(250, 249, 246, 1),
textTheme: const TextTheme(
labelMedium: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
color: Colors.black,
),
titleMedium: TextStyle(
fontSize: 16,
color: Color.fromRGBO(60, 60, 59, 1),
fontWeight: FontWeight.w700,
),
),
inputDecorationTheme: const InputDecorationTheme(
fillColor: Colors.white,
),
colorScheme: const ColorScheme.light(
primary: Color.fromRGBO(161, 203, 211, 1),
secondary: Color.fromRGBO(221, 235, 238, 1),
surface: Color.fromRGBO(255, 255, 255, 1),
),
appBarTheme: const AppBarTheme(
backgroundColor: Color.fromRGBO(161, 220, 218, 1),
foregroundColor: Colors.black,
titleTextStyle: TextStyle(
fontSize: 28,
color: Colors.white,
),
),
filledButtonTheme: FilledButtonThemeData(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
Colors.yellow,
),
foregroundColor: WidgetStateProperty.all(
Colors.black,
),
),
),
);

View file

@ -1,28 +0,0 @@
name: amazon
description: "A new Flutter project."
publish_to: 'none'
version: 1.0.0
environment:
sdk: '>=3.4.1 <4.0.0'
dependencies:
flutter:
sdk: flutter
flutter_hooks: ^0.20.0
hooks_riverpod: ^2.1.1
go_router: 12.1.3
flutter_nested_categories:
git:
url: https://github.com/Iconica-Development/flutter_nested_categories
ref: 0.0.1
flutter_shopping:
path: ../
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true

View file

@ -1,11 +0,0 @@
// SPDX-FileCopyrightText: 2022 Iconica
//
// SPDX-License-Identifier: BSD-3-Clause
import "package:flutter_test/flutter_test.dart";
void main() {
test("", () {
expect(true, true);
});
}

View file

@ -5,7 +5,5 @@ export "package:flutter_order_details/flutter_order_details.dart";
export "package:flutter_product_page/flutter_product_page.dart";
export "package:flutter_shopping_cart/flutter_shopping_cart.dart";
export "src/config/flutter_shopping_configuration.dart";
export "src/routes.dart";
export "src/user_stores/flutter_shopping_userstory_go_router.dart";
export "src/user_stores/flutter_shopping_userstory_navigation.dart";
export "src/configuration/shopping_configuration.dart";
export "src/flutter_shopping_navigator_userstory.dart";

View file

@ -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<FormState>(),
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<FormState>(),
stepName: "Address Information",
fields: [
OrderAddressInput(
title: "Your address",
outputKey: "address",
textController: TextEditingController(),
),
],
),
OrderDetailStep(
formKey: GlobalKey<FormState>(),
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),
),
),
);

View file

@ -1,43 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
/// Configuration class for the flutter_shopping user-story.
class FlutterShoppingConfiguration {
/// Constructor for the FlutterShoppingConfiguration.
const FlutterShoppingConfiguration({
required this.shopBuilder,
required this.shoppingCartBuilder,
required this.onCompleteUserStory,
this.orderDetailsBuilder,
this.onCompleteOrderDetails,
this.orderSuccessBuilder,
this.orderFailedBuilder,
});
/// Builder for the shop/product page.
final Widget Function(
BuildContext context,
String? initialBuildShopId,
String? streetName,
) shopBuilder;
/// Builder for the shopping cart page.
final Widget Function(BuildContext context) shoppingCartBuilder;
/// Function that is called when the user-story is completed.
final Function(BuildContext context) onCompleteUserStory;
/// Builder for the order details page. This does not have to be set if you
/// are using the default order details page.
final Widget Function(BuildContext context)? orderDetailsBuilder;
/// Allows you to execute actions before
final Future<bool> Function(BuildContext context, OrderResult result)?
onCompleteOrderDetails;
/// Builder for when the order is successful.
final Widget Function(BuildContext context)? orderSuccessBuilder;
/// Builder for when the order failed.
final Widget Function(BuildContext context)? orderFailedBuilder;
}

View file

@ -0,0 +1,240 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// configuration for the shopping userstory
class ShoppingConfiguration {
/// constructor for the userstory configuration
const ShoppingConfiguration({
/// ProductPage configurations
required this.shoppingService,
this.onGetProducts,
this.onGetShops,
this.onAddToCart,
this.onNavigateToShoppingCart,
this.getProductsInShoppingCart,
this.shoppingCartButtonBuilder,
this.productBuilder,
this.onShopSelectionChange,
this.productPageTranslations,
this.shopSelectorStyle,
this.productPagePagePadding,
this.productPageAppBarBuilder,
this.bottomNavigationBarBuilder,
this.onProductDetail,
this.discountDescription,
this.noContentBuilder,
this.errorBuilder,
this.categoryListBuilder,
this.shopselectorBuilder,
this.discountBuilder,
this.selectedCategoryBuilder,
/// ShoppingCart configurations
this.onConfirmOrder,
this.productItemBuilder,
this.confirmOrderButtonBuilder,
this.confirmOrderButtonHeight,
this.sumBottomSheetBuilder,
this.sumBottomSheetHeight,
this.titleBuilder,
this.shoppingCartTranslations,
this.shoppingCartPagePadding,
this.shoppingCartBottomPadding,
this.shoppingCartAppBarBuilder,
/// OrderDetail configurations
this.onNextStep,
this.onStepsCompleted,
this.onCompleteOrderDetails,
this.pages,
this.orderDetailTranslations,
this.orderDetailAppBarBuilder,
this.orderDetailNextbuttonBuilder,
this.orderSuccessBuilder,
});
/// The service that will be used for the userstory
final ShoppingService shoppingService;
/// Builder for the list of selected categories
final Widget Function(ProductPageConfiguration configuration)?
selectedCategoryBuilder;
/// Function that will be called when the products are requested
final Future<List<Product>> Function(String shopId)? onGetProducts;
/// Function that will be called when the shops are requested
final Future<List<Shop>> Function()? onGetShops;
/// Function that will be called when an item is added to the shopping cart
final Function(Product)? onAddToCart;
/// Function that will be called when the user navigates to the shopping cart
final Function()? onNavigateToShoppingCart;
/// Function that will be called to get the amount of
/// products in the shopping cart
final int Function()? getProductsInShoppingCart;
/// Default shopping cart button builder
final Widget Function(BuildContext, ProductPageConfiguration)?
shoppingCartButtonBuilder;
/// ProductPage item builder
final Widget Function(
BuildContext,
Product,
ProductPageConfiguration configuration,
)? productBuilder;
/// Function that will be called when the shop selection changes
final Function(Shop)? onShopSelectionChange;
/// Translations for the product page
final ProductPageTranslations? productPageTranslations;
/// Shop selector style
final ShopSelectorStyle? shopSelectorStyle;
/// ProductPage padding
final EdgeInsets? productPagePagePadding;
/// AppBar builder
final AppBar Function(BuildContext)? productPageAppBarBuilder;
/// BottomNavigationBarBuilder
final Widget? bottomNavigationBarBuilder;
/// Function that will be called when the product detail is requested
final Function(BuildContext, Product, String)? onProductDetail;
/// Function that will be called when the discount description is requested
final String Function(Product)? discountDescription;
/// Function that will be called when there are no products
final Widget Function(BuildContext)? noContentBuilder;
/// Function that will be called when there is an error
final Widget Function(BuildContext, Object?)? errorBuilder;
/// Builder for the shop selector. This builder is used to build the shop
/// selector that will be displayed in the product page.
final Widget Function(
BuildContext context,
ProductPageConfiguration configuration,
List<Shop> shops,
Function(Shop shop) onShopSelectionChange,
)? shopselectorBuilder;
/// Builder for the discount widget. This builder is used to build the
/// discount widget that will be displayed in the product page.
final Widget Function(
BuildContext context,
ProductPageConfiguration configuration,
List<Product> discountedProducts,
)? discountBuilder;
/// Builder for the list of items that are displayed in the product page.
final Widget Function(
BuildContext context,
ProductPageConfiguration configuration,
List<Product> products,
)? categoryListBuilder;
/// Function that will be called when the order button on
/// the shopping cart page is pressed
final Function(List<Product>)? onConfirmOrder;
/// Shopping cart item builder
final Widget Function(BuildContext, Product, ShoppingCartConfig)?
productItemBuilder;
/// Shopping cart confirm order button builder
final Widget Function(
BuildContext,
ShoppingCartConfig,
dynamic Function(List<Product>),
)? confirmOrderButtonBuilder;
/// The height of the confirm order button
/// This will not set the height of the button itself
/// this is only used to create some extra space on the bottom
/// of the product list so the button doesn't overlap with the
/// last product
final double? confirmOrderButtonHeight;
/// Shopping cart sum bottom sheet builder
final Widget Function(BuildContext, ShoppingCartConfig)?
sumBottomSheetBuilder;
/// The height of the sum bottom sheet
/// This will not set the height of the sheet itself
/// this is only used to create some extra space on the bottom
/// of the product list so the sheet doesn't overlap with the
/// last product
final double? sumBottomSheetHeight;
/// Function to override the title on the shopping cart screen
final Widget Function(BuildContext, String)? titleBuilder;
/// Shopping cart translations
final ShoppingCartTranslations? shoppingCartTranslations;
/// Shopping cart page padding
final EdgeInsets? shoppingCartPagePadding;
/// Shopping cart bottom padding
final EdgeInsets? shoppingCartBottomPadding;
/// Shopping cart app bar builder
final AppBar Function(BuildContext)? shoppingCartAppBarBuilder;
/// Function that gets called when the user navigates to the next
/// step of the order details
final dynamic Function(
int,
Map<String, dynamic>,
FlutterFormController controller,
)? onNextStep;
/// Function that gets called when the Navigates
/// to the order confirmationp page
final dynamic Function(
String,
List<Product>,
Map<int, Map<String, dynamic>>,
OrderDetailConfiguration,
)? onStepsCompleted;
/// Function that gets called when pressing the complete order
/// button on the confirmation page
final Function(BuildContext, OrderDetailConfiguration)?
onCompleteOrderDetails;
/// The order detail pages that are used in the order detail screen
final List<FlutterFormPage> Function(BuildContext)? pages;
/// The translations for the order detail screen
final OrderDetailTranslations? orderDetailTranslations;
/// The app bar for the order detail screen
final AppBar Function(BuildContext, String)? orderDetailAppBarBuilder;
/// The builder for the next button on the order detail screen
final Widget Function(
int,
// ignore: avoid_positional_boolean_parameters
bool,
BuildContext,
OrderDetailConfiguration,
FlutterFormController,
)? orderDetailNextbuttonBuilder;
/// The builder for the order success screen
final Widget? Function(
BuildContext,
OrderDetailConfiguration,
Map<int, Map<String, dynamic>>,
)? orderSuccessBuilder;
}

View file

@ -0,0 +1,235 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_shopping_local/flutter_shopping_local.dart";
/// User story for the shopping navigator.
class ShoppingNavigatorUserStory extends StatelessWidget {
/// Constructor for the shopping navigator user story.
const ShoppingNavigatorUserStory({
this.shoppingConfiguration,
super.key,
});
/// Shopping configuration.
final ShoppingConfiguration? shoppingConfiguration;
@override
Widget build(BuildContext context) => ShoppingProductPage(
shoppingConfiguration: shoppingConfiguration ??
ShoppingConfiguration(
shoppingService: LocalShoppingService(),
),
);
}
/// Shopping product page.
class ShoppingProductPage extends StatelessWidget {
/// Constructor for the shopping product page.
const ShoppingProductPage({
required this.shoppingConfiguration,
super.key,
});
/// Shopping configuration.
final ShoppingConfiguration shoppingConfiguration;
@override
Widget build(BuildContext context) {
var service = shoppingConfiguration.shoppingService;
return ProductPageScreen(
configuration: ProductPageConfiguration(
shoppingService: service,
shoppingCartButtonBuilder:
shoppingConfiguration.shoppingCartButtonBuilder,
productBuilder: shoppingConfiguration.productBuilder,
onShopSelectionChange: shoppingConfiguration.onShopSelectionChange,
translations: shoppingConfiguration.productPageTranslations ??
const ProductPageTranslations(),
shopSelectorStyle:
shoppingConfiguration.shopSelectorStyle ?? ShopSelectorStyle.row,
pagePadding: shoppingConfiguration.productPagePagePadding ??
const EdgeInsets.all(4),
appBarBuilder: shoppingConfiguration.productPageAppBarBuilder,
bottomNavigationBar: shoppingConfiguration.bottomNavigationBarBuilder,
onProductDetail: shoppingConfiguration.onProductDetail,
discountDescription: shoppingConfiguration.discountDescription,
noContentBuilder: shoppingConfiguration.noContentBuilder,
errorBuilder: shoppingConfiguration.errorBuilder,
shopselectorBuilder: shoppingConfiguration.shopselectorBuilder,
discountBuilder: shoppingConfiguration.discountBuilder,
categoryListBuilder: shoppingConfiguration.categoryListBuilder,
selectedCategoryBuilder: shoppingConfiguration.selectedCategoryBuilder,
shops: () async {
if (shoppingConfiguration.onGetShops != null) {
return shoppingConfiguration.onGetShops!();
} else {
return service.shopService.getShops();
}
},
getProducts: (shop) async {
if (shoppingConfiguration.onGetProducts != null) {
return shoppingConfiguration.onGetProducts!(shop.id);
} else {
return service.productService.getProducts(shop.id);
}
},
onAddToCart: (product) {
if (shoppingConfiguration.onAddToCart != null) {
shoppingConfiguration.onAddToCart!(product);
return;
} else {
return service.shoppingCartService.addProduct(product);
}
},
onNavigateToShoppingCart: () async {
if (shoppingConfiguration.onNavigateToShoppingCart != null) {
return shoppingConfiguration.onNavigateToShoppingCart!();
} else {
return Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ShoppingCart(
shoppingConfiguration: shoppingConfiguration,
),
),
);
}
},
getProductsInShoppingCart: () {
if (shoppingConfiguration.getProductsInShoppingCart != null) {
return shoppingConfiguration.getProductsInShoppingCart!();
} else {
return service.shoppingCartService.countProducts();
}
},
),
);
}
}
/// Shopping cart.
class ShoppingCart extends StatelessWidget {
/// Constructor for the shopping cart.
const ShoppingCart({
required this.shoppingConfiguration,
super.key,
});
/// Shopping configuration.
final ShoppingConfiguration shoppingConfiguration;
@override
Widget build(BuildContext context) {
var service = shoppingConfiguration.shoppingService.shoppingCartService;
return ShoppingCartScreen(
configuration: ShoppingCartConfig(
service: service,
productItemBuilder: shoppingConfiguration.productItemBuilder,
confirmOrderButtonBuilder:
shoppingConfiguration.confirmOrderButtonBuilder,
confirmOrderButtonHeight:
shoppingConfiguration.confirmOrderButtonHeight ?? 100,
sumBottomSheetBuilder: shoppingConfiguration.sumBottomSheetBuilder,
sumBottomSheetHeight: shoppingConfiguration.sumBottomSheetHeight ?? 100,
titleBuilder: shoppingConfiguration.titleBuilder,
translations: shoppingConfiguration.shoppingCartTranslations ??
const ShoppingCartTranslations(),
pagePadding: shoppingConfiguration.shoppingCartPagePadding ??
const EdgeInsets.symmetric(horizontal: 32),
bottomPadding: shoppingConfiguration.shoppingCartBottomPadding ??
const EdgeInsets.fromLTRB(44, 0, 44, 32),
appBarBuilder: shoppingConfiguration.shoppingCartAppBarBuilder,
onConfirmOrder: (products) async {
if (shoppingConfiguration.onConfirmOrder != null) {
return shoppingConfiguration.onConfirmOrder!(products);
} else {
return Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ShoppingOrderDetails(
shoppingConfiguration: shoppingConfiguration,
),
),
);
}
},
),
);
}
}
/// Shopping order details.
class ShoppingOrderDetails extends StatelessWidget {
/// Constructor for the shopping order details.
const ShoppingOrderDetails({
required this.shoppingConfiguration,
super.key,
});
/// Shopping configuration.
final ShoppingConfiguration shoppingConfiguration;
@override
Widget build(BuildContext context) => OrderDetailScreen(
configuration: OrderDetailConfiguration(
shoppingService: shoppingConfiguration.shoppingService,
pages: shoppingConfiguration.pages,
translations: shoppingConfiguration.orderDetailTranslations ??
const OrderDetailTranslations(),
appBarBuilder: shoppingConfiguration.orderDetailAppBarBuilder,
nextbuttonBuilder: shoppingConfiguration.orderDetailNextbuttonBuilder,
orderSuccessBuilder: (context, configuration, data) =>
shoppingConfiguration.orderSuccessBuilder
?.call(context, configuration, data) ??
DefaultOrderSucces(
configuration: configuration,
orderDetails: data,
),
onNextStep: (currentStep, data, controller) async {
if (shoppingConfiguration.onNextStep != null) {
return shoppingConfiguration.onNextStep!(
currentStep,
data,
controller,
);
} else {
await controller.autoNextStep();
}
},
onStepsCompleted: (shopId, products, data, configuration) async {
if (shoppingConfiguration.onStepsCompleted != null) {
return shoppingConfiguration.onStepsCompleted!(
shopId,
products,
data,
configuration,
);
} else {
return Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => DefaultOrderSucces(
configuration: configuration,
orderDetails: data,
),
),
);
}
},
onCompleteOrderDetails: (context, configuration) async {
if (shoppingConfiguration.onCompleteOrderDetails != null) {
return shoppingConfiguration.onCompleteOrderDetails!(
context,
configuration,
);
} else {
shoppingConfiguration.shoppingService.shoppingCartService.clear();
return Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => ShoppingProductPage(
shoppingConfiguration: shoppingConfiguration,
),
),
);
}
},
),
);
}

View file

@ -1,28 +0,0 @@
import "package:flutter/material.dart";
import "package:go_router/go_router.dart";
/// Builder with a fade transition for when navigating to a new screen.
CustomTransitionPage buildScreenWithFadeTransition<T>({
required BuildContext context,
required GoRouterState state,
required Widget child,
}) =>
CustomTransitionPage<T>(
key: state.pageKey,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
FadeTransition(opacity: animation, child: child),
);
/// Builder without a transition for when navigating to a new screen.
CustomTransitionPage buildScreenWithoutTransition<T>({
required BuildContext context,
required GoRouterState state,
required Widget child,
}) =>
CustomTransitionPage<T>(
key: state.pageKey,
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) =>
child,
);

View file

@ -1,35 +0,0 @@
/// All the name routes used in the user-story.
mixin FlutterShoppingNameRoutes {
/// The shop name route.
static const String shop = "shop";
/// The shopping cart name route.
static const String shoppingCart = "shoppingcart";
/// The order details name route.
static const String orderDetails = "orderdetails";
/// The order success name route.
static const String orderSuccess = "ordersuccess";
/// The order failed name route.
static const String orderFailed = "orderfailed";
}
/// All the path routes used in the user-story.
mixin FlutterShoppingPathRoutes {
/// The shop page route.
static const String shop = "/shop";
/// The shopping cart page route.
static const String shoppingCart = "/shopping-cart";
/// The order details page route.
static const String orderDetails = "/order-details";
/// The order success page route.
static const String orderSuccess = "/order-success";
/// The order failed page route.
static const String orderFailed = "/order-failed";
}

View file

@ -1,50 +0,0 @@
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";
/// All the routes for the shopping story.
List<GoRoute> getShoppingStoryRoutes({
required FlutterShoppingConfiguration configuration,
}) =>
<GoRoute>[
GoRoute(
name: FlutterShoppingNameRoutes.shop,
path: FlutterShoppingPathRoutes.shop,
builder: (context, state) => configuration.shopBuilder(
context,
state.uri.queryParameters["id"],
state.uri.queryParameters["street"],
),
),
GoRoute(
name: FlutterShoppingNameRoutes.shoppingCart,
path: FlutterShoppingPathRoutes.shoppingCart,
builder: (context, state) => configuration.shoppingCartBuilder(context),
),
GoRoute(
name: FlutterShoppingNameRoutes.orderDetails,
path: FlutterShoppingPathRoutes.orderDetails,
builder: (context, state) => configuration.orderDetailsBuilder != null
? configuration.orderDetailsBuilder!(context)
: OrderDetailScreen(
configuration:
getDefaultOrderDetailConfiguration(context, configuration),
),
),
GoRoute(
name: FlutterShoppingNameRoutes.orderSuccess,
path: FlutterShoppingPathRoutes.orderSuccess,
builder: (context, state) => configuration.orderSuccessBuilder != null
? configuration.orderSuccessBuilder!(context)
: DefaultOrderSucces(configuration: configuration),
),
GoRoute(
name: FlutterShoppingNameRoutes.orderFailed,
path: FlutterShoppingPathRoutes.orderFailed,
builder: (context, state) => configuration.orderFailedBuilder != null
? configuration.orderFailedBuilder!(context)
: DefaultOrderFailed(configuration: configuration),
),
];

View file

@ -1,52 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:go_router/go_router.dart";
/// Default on complete order details function.
/// This function will navigate to the order success or order failed page.
///
/// You can create your own implementation if you decide to use a different
/// approach.
Future<void> onCompleteOrderDetails(
BuildContext context,
FlutterShoppingConfiguration configuration,
OrderResult result,
) async {
var go = context.go;
var succesful = true;
if (configuration.onCompleteOrderDetails != null) {
var executionResult =
await configuration.onCompleteOrderDetails?.call(context, result);
if (executionResult == null || !executionResult) {
succesful = false;
}
}
if (succesful) {
go(FlutterShoppingPathRoutes.orderSuccess);
} else {
go(FlutterShoppingPathRoutes.orderFailed);
}
}
/// Default on complete shopping cart function.
///
/// You can create your own implementation if you decide to use a different
/// approach.
void onCompleteShoppingCart(
BuildContext context,
) {
context.go(FlutterShoppingPathRoutes.orderDetails);
}
/// Default on complete product page function.
///
/// You can create your own implementation if you decide to use a different
/// approach.
void onCompleteProductPage(
BuildContext context,
) {
context.go(FlutterShoppingPathRoutes.shoppingCart);
}

View file

@ -1,65 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// Default order failed widget.
class DefaultOrderFailed extends StatelessWidget {
/// Constructor for the DefaultOrderFailed.
const DefaultOrderFailed({
required this.configuration,
super.key,
});
/// Configuration for the user-story.
final FlutterShoppingConfiguration configuration;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var finishOrderButton = FilledButton(
onPressed: () => configuration.onCompleteUserStory(context),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32.0,
vertical: 8.0,
),
child: Text("Go back".toUpperCase()),
),
);
var content = Column(
children: [
const Spacer(),
const Icon(
Icons.error,
size: 100,
color: Colors.red,
),
const SizedBox(height: 16),
Text(
"Uh oh.",
style: theme.textTheme.titleLarge,
),
const SizedBox(height: 32),
Text(
"It seems that something went wrong.",
style: theme.textTheme.bodyMedium,
),
Text(
"Please try again later.",
style: theme.textTheme.bodyMedium,
),
const Spacer(),
finishOrderButton,
],
);
return Scaffold(
body: SafeArea(
child: Center(
child: content,
),
),
);
}
}

View file

@ -1,66 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// Default order success widget.
class DefaultOrderSucces extends StatelessWidget {
/// Constructor for the DefaultOrderSucces.
const DefaultOrderSucces({
required this.configuration,
super.key,
});
/// Configuration for the user-story.
final FlutterShoppingConfiguration configuration;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var finishOrderButton = FilledButton(
onPressed: () => configuration.onCompleteUserStory(context),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32.0,
vertical: 8.0,
),
child: Text("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(
body: SafeArea(
child: Center(
child: content,
),
),
);
}
}

View file

@ -1,10 +1,10 @@
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'
sdk: ">=3.3.4 <4.0.0"
dependencies:
flutter:
@ -13,18 +13,28 @@ 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
flutter_shopping_interface:
git:
url: https://github.com/Iconica-Development/flutter_shopping
ref: 2.0.0
path: packages/flutter_shopping_interface
flutter_shopping_local:
git:
url: https://github.com/Iconica-Development/flutter_shopping
ref: 2.0.0
path: packages/flutter_shopping_local
dev_dependencies:
flutter_test:
@ -35,13 +45,3 @@ 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

View file

@ -2,7 +2,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/config/shopping_cart_translations.dart";
export "src/widgets/shopping_cart_screen.dart";

View file

@ -1,133 +1,89 @@
import "package:flutter/material.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
Widget _defaultNoContentBuilder(BuildContext context) =>
const SizedBox.shrink();
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Shopping cart configuration
///
/// This class is used to configure the shopping cart.
class ShoppingCartConfig<T extends ShoppingCartProduct> {
class ShoppingCartConfig {
/// Creates a shopping cart configuration.
ShoppingCartConfig({
required this.productService,
//
this.onConfirmOrder,
required this.service,
required this.onConfirmOrder,
this.productItemBuilder,
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.translations = const ShoppingCartTranslations(),
this.pagePadding = const EdgeInsets.symmetric(horizontal: 32),
this.bottomPadding = const EdgeInsets.fromLTRB(44, 0, 44, 32),
//
this.appBar,
//
Widget Function(BuildContext context, Locale locale, T product)?
productItemBuilder,
Widget Function(BuildContext context) noContentBuilder =
_defaultNoContentBuilder,
}) : assert(
confirmOrderButtonBuilder != null || onConfirmOrder != null,
"""
If you override the confirm order button builder,
you cannot use the onConfirmOrder callback.""",
),
assert(
confirmOrderButtonBuilder == null || onConfirmOrder == null,
"""
If you do not override the confirm order button builder,
you must use the onConfirmOrder callback.""",
),
_noContentBuilder = noContentBuilder {
_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),
),
);
}
this.appBarBuilder,
});
/// Product Service. The service contains all the products that
/// a shopping cart can contain. Each product must extend the [Product] class.
/// The service is used to add, remove, and update products.
///
/// The service can be seperate for each shopping cart in-case you want to
/// support seperate shopping carts for shop.
ProductService<T> productService = ProductService<T>(<T>[]);
late final Widget Function(BuildContext context, Locale locale, T product)?
_productItemBuilder;
/// Product service. The product service is used to manage the products in the
/// shopping cart.
final ShoppingCartService service;
/// Product item builder. This builder is used to build the product item
/// that will be displayed in the shopping cart.
Widget Function(BuildContext context, Locale locale, T product)
get productItemBuilder => _productItemBuilder!;
final Widget Function(BuildContext context) _noContentBuilder;
/// No content builder. This builder is used to build the no content widget
/// that will be displayed in the shopping cart when there are no products.
Widget Function(BuildContext context) get noContentBuilder =>
_noContentBuilder;
Widget Function(
BuildContext context,
Product product,
ShoppingCartConfig configuration,
)? productItemBuilder;
/// Confirm order button builder. This builder is used to build the confirm
/// order button that will be displayed in the shopping cart.
/// If you override this builder, you cannot use the [onConfirmOrder] callback
final Widget Function(BuildContext context)? confirmOrderButtonBuilder;
Widget Function(
BuildContext context,
ShoppingCartConfig configuration,
Function(List<Product> products) onConfirmOrder,
)? confirmOrderButtonBuilder;
/// Confirm order button height. The height of the confirm order button.
/// This height is used to calculate the bottom padding of the shopping cart.
/// If you override the confirm order button builder, you must provide a
/// height.
final double confirmOrderButtonHeight;
double confirmOrderButtonHeight;
/// Confirm order callback. This callback is called when the confirm order
/// button is pressed. The callback will not be called if you override the
/// confirm order button builder.
final Function(List<T> products)? onConfirmOrder;
final Function(List<Product> products) onConfirmOrder;
/// Sum bottom sheet builder. This builder is used to build the sum bottom
/// sheet that will be displayed in the shopping cart. The sum bottom sheet
/// can be used to display the total sum of the products in the shopping cart.
final Widget Function(BuildContext context)? sumBottomSheetBuilder;
Widget Function(BuildContext context, ShoppingCartConfig configuration)?
sumBottomSheetBuilder;
/// Sum bottom sheet height. The height of the sum bottom sheet.
/// This height is used to calculate the bottom padding of the shopping cart.
/// If you override the sum bottom sheet builder, you must provide a height.
final double sumBottomSheetHeight;
double sumBottomSheetHeight;
/// Padding around the shopping cart. The padding is used to create space
/// around the shopping cart.
final EdgeInsets padding;
EdgeInsets pagePadding;
/// Bottom padding of the shopping cart. The bottom padding is used to create
/// a padding around the bottom sheet. This padding is ignored when the
/// [sumBottomSheetBuilder] is overridden.
final EdgeInsets bottomPadding;
EdgeInsets bottomPadding;
/// Title 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.
final Widget Function(
BuildContext context,
String title,
)? titleBuilder;
/// Title builder. This builder is used to build the title of the shopping
/// cart. The title is displayed at the top of the shopping cart. If you
/// use the title builder, the [title] will be ignored.
final Widget Function(BuildContext context)? titleBuilder;
/// Shopping cart translations. The translations for the shopping cart.
ShoppingCartTranslations translations;
/// Shopping cart localizations. The localizations are used to localize the
/// shopping cart.
final ShoppingCartLocalizations localizations;
/// App bar for the shopping cart screen.
final PreferredSizeWidget? appBar;
/// Appbar for the shopping cart screen.
PreferredSizeWidget Function(BuildContext context)? appBarBuilder;
}

View file

@ -1,24 +0,0 @@
import "package:flutter/material.dart";
/// Shopping cart localizations
class ShoppingCartLocalizations {
/// Creates shopping cart localizations
const ShoppingCartLocalizations({
this.locale = const Locale("en", "US"),
this.placeOrder = "PLACE ORDER",
this.sum = "Total:",
});
/// Locale for the shopping cart.
/// This locale will be used to format the currency.
/// Default is English.
final Locale locale;
/// Localization for the place order button.
/// This text will only be displayed if you're not using the place order
/// button builder.
final String placeOrder;
/// Localization for the sum.
final String sum;
}

View file

@ -0,0 +1,23 @@
/// Shopping cart localizations
class ShoppingCartTranslations {
/// Creates shopping cart localizations
const ShoppingCartTranslations({
this.placeOrder = "Order",
this.sum = "Subtotal:",
this.cartTitle = "Products",
this.close = "close",
});
/// Text for the place order button.
final String placeOrder;
/// Localization for the sum.
final String sum;
/// Title for the shopping cart. This title will be displayed at the top of
/// the shopping cart.
final String cartTitle;
/// Localization for the close button for the popup.
final String close;
}

View file

@ -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;
}

View file

@ -1,71 +0,0 @@
import "package:flutter/foundation.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
/// Product service. This class is responsible for managing the products.
/// The service is used to add, remove, and update products.
class ProductService<T extends ShoppingCartProduct> extends ChangeNotifier {
/// Creates a product service.
ProductService(this.products);
/// List of products in the shopping cart.
final List<T> products;
/// Adds a product to the shopping cart.
void addProduct(T product) {
for (var p in products) {
if (p.id == product.id) {
p.quantity++;
notifyListeners();
return;
}
}
products.add(product);
notifyListeners();
}
/// Removes a product from the shopping cart.
void removeProduct(T product) {
for (var p in products) {
if (p.id == product.id) {
products.remove(p);
notifyListeners();
return;
}
}
notifyListeners();
}
/// Removes one product from the shopping cart.
void removeOneProduct(T product) {
for (var p in products) {
if (p.id == product.id) {
if (p.quantity > 1) {
p.quantity--;
notifyListeners();
return;
}
}
}
products.remove(product);
notifyListeners();
}
/// Counts the number of products in the shopping cart.
int countProducts() {
var count = 0;
for (var product in products) {
count += product.quantity;
}
return count;
}
/// Empties the shopping cart.
void clear() {
products.clear();
notifyListeners();
}
}

View file

@ -0,0 +1,23 @@
import "package:flutter/material.dart";
/// Default appbar for the shopping cart.
class DefaultAppbar extends StatelessWidget implements PreferredSizeWidget {
/// Constructor for the default appbar for the shopping cart.
const DefaultAppbar({
super.key,
});
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return AppBar(
title: Text(
"Shopping cart",
style: theme.textTheme.headlineLarge,
),
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

Some files were not shown because too many files have changed in this diff Show more