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>
This commit is contained in:
mike doornenbal 2024-07-02 13:39:36 +02:00 committed by GitHub
parent 0838b7b017
commit 5a24f7cf6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 1445 additions and 2421 deletions

View file

@ -3,15 +3,6 @@ library flutter_order_details;
export "src/configuration/order_detail_configuration.dart"; export "src/configuration/order_detail_configuration.dart";
export "src/configuration/order_detail_localization.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/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_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/widgets/order_detail_screen.dart";

View file

@ -1,50 +1,537 @@
import "package:flutter/widgets.dart"; // ignore_for_file: avoid_annotating_with_dynamic
import "package:flutter_order_details/src/configuration/order_detail_localization.dart";
import "package:flutter_order_details/src/configuration/order_detail_step.dart"; import "package:animated_toggle/animated_toggle.dart";
import "package:flutter_order_details/src/models/order_result.dart"; import "package:flutter/material.dart";
import "package:flutter_form_wizard/flutter_form.dart";
import "package:flutter_order_details/flutter_order_details.dart";
/// Configuration for the order detail screen. /// Configuration for the order detail screen.
class OrderDetailConfiguration { class OrderDetailConfiguration {
/// Constructor for the order detail configuration. /// Constructor for the order detail configuration.
const OrderDetailConfiguration({ OrderDetailConfiguration({
required this.steps,
//
required this.onCompleted, required this.onCompleted,
// this.pages = _defaultPages,
this.progressIndicator = true,
//
this.localization = const OrderDetailLocalization(), this.localization = const OrderDetailLocalization(),
// this.appBar = _defaultAppBar,
this.inputFieldPadding = const EdgeInsets.symmetric( this.nextbuttonBuilder = _defaultNextButtonBuilder,
horizontal: 32,
vertical: 16,
),
this.titlePadding = const EdgeInsets.only(left: 16, right: 16, top: 16),
//
this.appBar,
}); });
/// The different steps that the user has to go through to complete the order. /// 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. /// 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. /// 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. /// The result of the order is passed as an argument to the function.
final Function(OrderResult result) onCompleted; final Function(dynamic value) onCompleted;
/// Whether or not you want to show a progress indicator at
/// the top of the screen.
final bool progressIndicator;
/// Localization for the order detail screen. /// Localization for the order detail screen.
final OrderDetailLocalization localization; final OrderDetailLocalization localization;
/// Padding around the input fields.
final EdgeInsets inputFieldPadding;
/// Padding around the title of the input fields.
final EdgeInsets titlePadding;
/// Optional app bar that you can pass to the order detail screen. /// Optional app bar that you can pass to the order detail screen.
final PreferredSizeWidget? appBar; final AppBar Function(
BuildContext context,
OrderDetailLocalization localizations,
) appBar;
/// Optional next button builder that you can pass to the order detail screen.
final Widget Function(
int a,
// ignore: avoid_positional_boolean_parameters
bool b,
BuildContext context,
OrderDetailConfiguration configuration,
FlutterFormController controller,
) nextbuttonBuilder;
}
AppBar _defaultAppBar(
BuildContext context,
OrderDetailLocalization localizations,
) {
var theme = Theme.of(context);
return AppBar(
title: Text(
localizations.orderDetailsTitle,
style: theme.textTheme.headlineLarge,
),
);
}
Widget _defaultNextButtonBuilder(
int currentStep,
bool b,
BuildContext context,
OrderDetailConfiguration configuration,
FlutterFormController controller,
) {
var theme = Theme.of(context);
var nextButtonTexts = [
"Choose date and time",
"Next",
"Next",
];
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 32),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () async {
controller.validateAndSaveCurrentStep();
await controller.autoNextStep();
},
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12,
),
child: Text(
nextButtonTexts[currentStep],
style: theme.textTheme.displayLarge,
),
),
),
),
),
);
}
List<FlutterFormPage> _defaultPages(BuildContext context) {
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;
},
childLeft: Center(
child: ListenableBuilder(
listenable: switchStatus,
builder: (context, widget) => Text(
"Morning",
style: theme.textTheme.titleSmall?.copyWith(
color: switchStatus.value
? theme.colorScheme.primary
: Colors.white,
),
),
),
),
childRight: Center(
child: ListenableBuilder(
listenable: switchStatus,
builder: (context, widget) => Text(
"Afternoon",
style: theme.textTheme.titleSmall?.copyWith(
color: switchStatus.value
? Colors.white
: theme.colorScheme.primary,
),
),
),
),
),
),
const SizedBox(
height: 8,
),
ListenableBuilder(
listenable: switchStatus,
builder: (context, widget) => FlutterFormInputMultipleChoice(
validationMessage: "Select a Time",
controller: multipleChoiceController,
options: switchStatus.value ? afternoonTimes : morningTimes,
mainAxisSpacing: 5,
crossAxisSpacing: 5,
childAspectRatio: 2,
height: MediaQuery.of(context).size.height * 0.6,
builder:
(context, index, selected, controller, options, state) =>
GestureDetector(
onTap: () {
state.didChange(options[index]);
selected.value = index;
controller.onSaved(options[index]);
},
child: Container(
decoration: BoxDecoration(
color: selected.value == index
? Theme.of(context).colorScheme.primary
: Colors.white,
borderRadius: BorderRadius.circular(10),
),
height: 40,
child: Center(
child: Text(options[index]),
),
),
),
),
),
],
),
),
),
FlutterFormPage(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Payment method",
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
Text(
"Choose when you would like to to pay for the order.",
style: theme.textTheme.bodyMedium,
),
const SizedBox(
height: 84,
),
FlutterFormInputMultipleChoice(
crossAxisCount: 1,
mainAxisSpacing: 24,
crossAxisSpacing: 5,
childAspectRatio: 2,
height: 420,
controller: FlutterFormInputMultipleChoiceController(
id: "payment",
mandatory: true,
),
options: const ["PAY NOW", "PAY AT THE CASHIER"],
builder: (context, index, selected, controller, options, state) =>
GestureDetector(
onTap: () {
state.didChange(options[index]);
selected.value = index;
controller.onSaved(options[index]);
},
child: Container(
decoration: BoxDecoration(
color: selected.value == index
? Theme.of(context).colorScheme.primary
: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Theme.of(context).colorScheme.primary,
),
),
height: 40,
child: Center(child: Text(options[index])),
),
),
validationMessage: "Please select a payment method",
),
],
),
),
),
];
} }

View file

@ -2,17 +2,17 @@
class OrderDetailLocalization { class OrderDetailLocalization {
/// Constructor for the order detail localization. /// Constructor for the order detail localization.
const OrderDetailLocalization({ const OrderDetailLocalization({
this.nextButton = "Next", this.nextButton = "Order",
this.backButton = "Back",
this.completeButton = "Complete", this.completeButton = "Complete",
this.orderDetailsTitle = "Information",
}); });
/// Next button localization. /// Next button localization.
final String nextButton; final String nextButton;
/// Back button localization.
final String backButton;
/// Complete button localization. /// Complete button localization.
final String completeButton; final String completeButton;
/// Title for the order details page.
final String orderDetailsTitle;
} }

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,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,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

@ -1,4 +1,5 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_form_wizard/flutter_form.dart";
import "package:flutter_order_details/flutter_order_details.dart"; import "package:flutter_order_details/flutter_order_details.dart";
/// Order Detail Screen. /// Order Detail Screen.
@ -17,257 +18,29 @@ class OrderDetailScreen extends StatefulWidget {
} }
class _OrderDetailScreenState extends State<OrderDetailScreen> { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var controller = FlutterFormController();
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( return Scaffold(
appBar: widget.configuration.appBar, appBar: widget.configuration.appBar
body: Stack( .call(context, widget.configuration.localization),
children: [ body: FlutterForm(
pageBody, formController: controller,
if (_blurBackground) pageBlur, options: FlutterFormOptions(
], nextButton: (a, b) => widget.configuration.nextbuttonBuilder(
), a,
); b,
}
}
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, context,
orderResult.order[input.outputKey], widget.configuration,
onBlurBackground, controller,
),
pages: widget.configuration.pages.call(context),
onFinished: (data) {
widget.configuration.onCompleted.call(data);
},
onNext: (step, data) {},
), ),
), ),
],
); );
} }
} }

View file

@ -1,6 +1,7 @@
name: flutter_order_details name: flutter_order_details
description: "A Flutter module for order details." description: "A Flutter module for order details."
version: 1.0.0 version: 2.0.0
publish_to: 'none'
environment: environment:
sdk: '>=3.3.0 <4.0.0' sdk: '>=3.3.0 <4.0.0'
@ -8,6 +9,14 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
animated_toggle:
git:
url: https://github.com/Iconica-Development/flutter_animated_toggle
ref: 0.0.3
flutter_form_wizard:
git:
url: https://github.com/Iconica-Development/flutter_form_wizard
ref: 6.5.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -18,13 +27,4 @@ dev_dependencies:
ref: 7.0.0 ref: 7.0.0
flutter: 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

@ -7,7 +7,6 @@ export "src/configuration/product_page_configuration.dart";
export "src/configuration/product_page_content.dart"; export "src/configuration/product_page_content.dart";
export "src/configuration/product_page_localization.dart"; export "src/configuration/product_page_localization.dart";
export "src/configuration/product_page_shop_selector_style.dart"; export "src/configuration/product_page_shop_selector_style.dart";
export "src/models/product.dart";
export "src/models/product_page_shop.dart"; export "src/models/product_page_shop.dart";
export "src/ui/product_page.dart"; export "src/ui/product_page.dart";
export "src/ui/product_page_screen.dart"; export "src/ui/product_page_screen.dart";

View file

@ -1,47 +1,38 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart"; import "package:flutter_product_page/src/services/shopping_cart_notifier.dart";
import "package:flutter_product_page/src/ui/widgets/product_item_popup.dart"; import "package:flutter_product_page/src/ui/widgets/product_item_popup.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// Configuration for the product page. /// Configuration for the product page.
class ProductPageConfiguration { class ProductPageConfiguration {
/// Constructor for the product page configuration. /// Constructor for the product page configuration.
ProductPageConfiguration({ ProductPageConfiguration({
required this.shops, required this.shops,
//
required this.getProducts, required this.getProducts,
//
required this.onAddToCart, required this.onAddToCart,
required this.onNavigateToShoppingCart, required this.onNavigateToShoppingCart,
this.navigateToShoppingCartBuilder, this.navigateToShoppingCartBuilder = _defaultNavigateToShoppingCartBuilder,
//
this.initialShopId, this.initialShopId,
//
this.productBuilder, this.productBuilder,
//
this.onShopSelectionChange, this.onShopSelectionChange,
this.getProductsInShoppingCart, this.getProductsInShoppingCart,
//
this.localizations = const ProductPageLocalization(), this.localizations = const ProductPageLocalization(),
//
this.shopSelectorStyle = ShopSelectorStyle.spacedWrap, this.shopSelectorStyle = ShopSelectorStyle.spacedWrap,
this.categoryStylingConfiguration = this.categoryStylingConfiguration =
const ProductPageCategoryStylingConfiguration(), const ProductPageCategoryStylingConfiguration(),
//
this.pagePadding = const EdgeInsets.all(4), this.pagePadding = const EdgeInsets.all(4),
// this.appBar = _defaultAppBar,
this.appBar,
this.bottomNavigationBar, this.bottomNavigationBar,
//
Function( Function(
BuildContext context, BuildContext context,
ProductPageProduct product, Product product,
)? onProductDetail, )? onProductDetail,
String Function( String Function(
ProductPageProduct product, Product product,
)? getDiscountDescription, )? getDiscountDescription,
Widget Function( Widget Function(
BuildContext context, BuildContext context,
ProductPageProduct product, Product product,
)? productPopupBuilder, )? productPopupBuilder,
Widget Function( Widget Function(
BuildContext context, BuildContext context,
@ -54,14 +45,13 @@ class ProductPageConfiguration {
}) { }) {
_productPopupBuilder = productPopupBuilder; _productPopupBuilder = productPopupBuilder;
_productPopupBuilder ??= _productPopupBuilder ??=
(BuildContext context, ProductPageProduct product) => ProductItemPopup( (BuildContext context, Product product) => ProductItemPopup(
product: product, product: product,
configuration: this, configuration: this,
); );
_onProductDetail = onProductDetail; _onProductDetail = onProductDetail;
_onProductDetail ??= _onProductDetail ??= (BuildContext context, Product product) async {
(BuildContext context, ProductPageProduct product) async {
var theme = Theme.of(context); var theme = Theme.of(context);
await showModalBottomSheet( await showModalBottomSheet(
@ -98,8 +88,8 @@ class ProductPageConfiguration {
}; };
_getDiscountDescription = getDiscountDescription; _getDiscountDescription = getDiscountDescription;
_getDiscountDescription ??= _getDiscountDescription ??= (Product product) =>
(ProductPageProduct product) => "${product.name} is on sale!"; "${product.name}, now for ${product.discountPrice} each";
} }
/// The shop that is initially selected. /// The shop that is initially selected.
@ -119,27 +109,25 @@ class ProductPageConfiguration {
/// for each product in their seperated category. This builder should only /// for each product in their seperated category. This builder should only
/// build the widget for one specific product. This builder has a default /// build the widget for one specific product. This builder has a default
/// in-case the developer does not override it. /// in-case the developer does not override it.
Widget Function(BuildContext context, ProductPageProduct product)? Widget Function(BuildContext context, Product product)? productBuilder;
productBuilder;
late Widget Function(BuildContext context, ProductPageProduct product)? late Widget Function(BuildContext context, Product product)?
_productPopupBuilder; _productPopupBuilder;
/// The builder for the product popup. This popup will be displayed when the /// 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 /// user clicks on a product. This builder should only build the widget that
/// displays the content of one specific product. /// displays the content of one specific product.
/// This builder has a default in-case the developer /// This builder has a default in-case the developer
Widget Function(BuildContext context, ProductPageProduct product) Widget Function(BuildContext context, Product product)
get productPopupBuilder => _productPopupBuilder!; get productPopupBuilder => _productPopupBuilder!;
late Function(BuildContext context, ProductPageProduct product)? late Function(BuildContext context, Product product)? _onProductDetail;
_onProductDetail;
/// This function handles the creation of the product detail popup. This /// This function handles the creation of the product detail popup. This
/// function has a default in-case the developer does not override it. /// function has a default in-case the developer does not override it.
/// The default intraction is a popup, but this can be overriden. /// The default intraction is a popup, but this can be overriden.
Function(BuildContext context, ProductPageProduct product) Function(BuildContext context, Product product) get onProductDetail =>
get onProductDetail => _onProductDetail!; _onProductDetail!;
late Widget Function(BuildContext context)? _noContentBuilder; late Widget Function(BuildContext context)? _noContentBuilder;
@ -149,7 +137,11 @@ class ProductPageConfiguration {
/// The builder for the shopping cart. This builder should return a widget /// The builder for the shopping cart. This builder should return a widget
/// that navigates to the shopping cart overview page. /// that navigates to the shopping cart overview page.
Widget Function(BuildContext context)? navigateToShoppingCartBuilder; Widget Function(
BuildContext context,
ProductPageConfiguration configuration,
ShoppingCartNotifier notifier,
) navigateToShoppingCartBuilder;
late Widget Function( late Widget Function(
BuildContext context, BuildContext context,
@ -162,16 +154,16 @@ class ProductPageConfiguration {
Widget Function(BuildContext context, Object? error, StackTrace? stackTrace)? Widget Function(BuildContext context, Object? error, StackTrace? stackTrace)?
get errorBuilder => _errorBuilder; get errorBuilder => _errorBuilder;
late String Function(ProductPageProduct product)? _getDiscountDescription; late String Function(Product product)? _getDiscountDescription;
/// The function that returns the description of the discount for a product. /// The function that returns the description of the discount for a product.
/// This allows you to translate and give custom messages for each product. /// This allows you to translate and give custom messages for each product.
String Function(ProductPageProduct product)? get getDiscountDescription => String Function(Product product)? get getDiscountDescription =>
_getDiscountDescription!; _getDiscountDescription!;
/// This function must be implemented by the developer and should handle the /// This function must be implemented by the developer and should handle the
/// adding of a product to the cart. /// 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 gets executed when the user changes the shop selection.
/// This function always fires upon first load with the initial shop as well. /// This function always fires upon first load with the initial shop as well.
@ -198,5 +190,60 @@ class ProductPageConfiguration {
final Widget? bottomNavigationBar; final Widget? bottomNavigationBar;
/// Optional app bar that you can pass to the order detail screen. /// Optional app bar that you can pass to the order detail screen.
final PreferredSizeWidget? appBar; final AppBar Function(BuildContext context)? appBar;
}
AppBar _defaultAppBar(
BuildContext context,
) {
var theme = Theme.of(context);
return AppBar(
leading: IconButton(onPressed: () {}, icon: const Icon(Icons.person)),
actions: [
IconButton(onPressed: () {}, icon: const Icon(Icons.filter_alt)),
],
title: Text(
"Product page",
style: theme.textTheme.headlineLarge,
),
);
}
Widget _defaultNavigateToShoppingCartBuilder(
BuildContext context,
ProductPageConfiguration configuration,
ShoppingCartNotifier notifier,
) {
var theme = Theme.of(context);
return ListenableBuilder(
listenable: notifier,
builder: (context, widget) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: configuration.getProductsInShoppingCart?.call() != 0
? configuration.onNavigateToShoppingCart
: null,
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12,
),
child: Text(
configuration.localizations.navigateToShoppingCart,
style: theme.textTheme.displayLarge,
),
),
),
),
),
);
} }

View file

@ -1,4 +1,4 @@
import "package:flutter_product_page/flutter_product_page.dart"; import "package:flutter_shopping/flutter_shopping.dart";
/// Return type that contains the products and an optional discounted product. /// Return type that contains the products and an optional discounted product.
class ProductPageContent { class ProductPageContent {
@ -9,8 +9,8 @@ class ProductPageContent {
}); });
/// List of products that belong to the shop. /// List of products that belong to the shop.
final List<ProductPageProduct> products; final List<Product> products;
/// Optional highlighted discounted product to display. /// Optional highlighted discounted product to display.
final ProductPageProduct? discountedProduct; final Product? discountedProduct;
} }

View file

@ -2,8 +2,8 @@
class ProductPageLocalization { class ProductPageLocalization {
/// Default constructor /// Default constructor
const ProductPageLocalization({ const ProductPageLocalization({
this.navigateToShoppingCart = "To shopping cart", this.navigateToShoppingCart = "View shopping cart",
this.discountTitle = "Discount", this.discountTitle = "Weekly offer",
this.failedToLoadImageExplenation = "Failed to load image", this.failedToLoadImageExplenation = "Failed to load image",
this.close = "Close", this.close = "Close",
}); });

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,14 +1,14 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_nested_categories/flutter_nested_categories.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/services/shopping_cart_notifier.dart";
import "package:flutter_product_page/src/ui/components/product_item.dart"; import "package:flutter_product_page/src/ui/components/product_item.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// A function that is called when a product is added to the cart. /// A function that is called when a product is added to the cart.
ProductPageProduct onAddToCartWrapper( Product onAddToCartWrapper(
ProductPageConfiguration configuration, ProductPageConfiguration configuration,
ShoppingCartNotifier shoppingCartNotifier, ShoppingCartNotifier shoppingCartNotifier,
ProductPageProduct product, Product product,
) { ) {
shoppingCartNotifier.productsChanged(); shoppingCartNotifier.productsChanged();
@ -19,13 +19,14 @@ ProductPageProduct onAddToCartWrapper(
/// Generates a [CategoryList] from a list of [Product]s and a /// Generates a [CategoryList] from a list of [Product]s and a
/// [ProductPageConfiguration]. /// [ProductPageConfiguration].
CategoryList getCategoryList( Widget getCategoryList(
BuildContext context, BuildContext context,
ProductPageConfiguration configuration, ProductPageConfiguration configuration,
ShoppingCartNotifier shoppingCartNotifier, 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) { for (var product in products) {
if (!categorizedProducts.containsKey(product.category)) { if (!categorizedProducts.containsKey(product.category)) {
categorizedProducts[product.category] = []; categorizedProducts[product.category] = [];
@ -43,8 +44,7 @@ CategoryList getCategoryList(
: ProductItem( : ProductItem(
product: product, product: product,
onProductDetail: configuration.onProductDetail, onProductDetail: configuration.onProductDetail,
onAddToCart: (ProductPageProduct product) => onAddToCart: (Product product) => onAddToCartWrapper(
onAddToCartWrapper(
configuration, configuration,
shoppingCartNotifier, shoppingCartNotifier,
product, product,
@ -59,15 +59,19 @@ CategoryList getCategoryList(
); );
categories.add(category); categories.add(category);
}); });
return Column(
return CategoryList( crossAxisAlignment: CrossAxisAlignment.start,
title: configuration.categoryStylingConfiguration.title, children: [
titleStyle: configuration.categoryStylingConfiguration.titleStyle, for (var category in categories) ...[
customTitle: configuration.categoryStylingConfiguration.customTitle, Text(
headerCentered: configuration.categoryStylingConfiguration.headerCentered, category.name!,
headerStyling: configuration.categoryStylingConfiguration.headerStyling, style: theme.textTheme.titleMedium,
isCategoryCollapsible: ),
configuration.categoryStylingConfiguration.isCategoryCollapsible, Column(
content: categories, children: category.content,
),
const SizedBox(height: 16),
],
],
); );
} }

View file

@ -1,6 +1,6 @@
import "package:cached_network_image/cached_network_image.dart"; import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart"; import "package:flutter_shopping/flutter_shopping.dart";
import "package:skeletonizer/skeletonizer.dart"; import "package:skeletonizer/skeletonizer.dart";
/// Product item widget. /// Product item widget.
@ -15,14 +15,13 @@ class ProductItem extends StatelessWidget {
}); });
/// Product to display. /// Product to display.
final ProductPageProduct product; final Product product;
/// Function to call when the product detail is requested. /// Function to call when the product detail is requested.
final Function(BuildContext context, ProductPageProduct selectedProduct) final Function(BuildContext context, Product selectedProduct) onProductDetail;
onProductDetail;
/// Function to call when the product is added to the cart. /// 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. /// Localizations for the product page.
final ProductPageLocalization localizations; final ProductPageLocalization localizations;
@ -76,7 +75,10 @@ class ProductItem extends StatelessWidget {
padding: const EdgeInsets.only(left: 4), padding: const EdgeInsets.only(left: 4),
child: IconButton( child: IconButton(
onPressed: () => onProductDetail(context, product), onPressed: () => onProductDetail(context, product),
icon: const Icon(Icons.info_outline), icon: Icon(
Icons.info_outline,
color: theme.colorScheme.primary,
),
), ),
); );
@ -84,10 +86,7 @@ class ProductItem extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
_PriceLabel( _PriceLabel(
price: product.price, product: product,
discountPrice: (product.hasDiscount && product.discountPrice != null)
? product.discountPrice
: null,
), ),
_AddToCardButton( _AddToCardButton(
product: product, product: product,
@ -113,39 +112,33 @@ class ProductItem extends StatelessWidget {
class _PriceLabel extends StatelessWidget { class _PriceLabel extends StatelessWidget {
const _PriceLabel({ const _PriceLabel({
required this.price, required this.product,
required this.discountPrice,
}); });
final double price; final Product product;
final double? discountPrice;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
if (discountPrice == null)
return Text(
price.toStringAsFixed(2),
style: theme.textTheme.bodyMedium,
);
else
return Row( return Row(
children: [ children: [
if (product.hasDiscount) ...[
Text( Text(
price.toStringAsFixed(2), product.price.toStringAsFixed(2),
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
fontSize: 10,
color: theme.colorScheme.primary,
decoration: TextDecoration.lineThrough, decoration: TextDecoration.lineThrough,
), ),
textAlign: TextAlign.center,
), ),
Padding( const SizedBox(width: 4),
padding: const EdgeInsets.only(left: 4.0), ],
child: Text( Text(
discountPrice!.toStringAsFixed(2), product.hasDiscount
style: theme.textTheme.bodyMedium, ? product.discountPrice!.toStringAsFixed(2)
), : product.price.toStringAsFixed(2),
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
), ),
], ],
); );
@ -158,36 +151,30 @@ class _AddToCardButton extends StatelessWidget {
required this.onAddToCart, required this.onAddToCart,
}); });
final ProductPageProduct product; final Product product;
final Function(ProductPageProduct product) onAddToCart; final Function(Product product) onAddToCart;
static const double boxSize = 29; static const double boxSize = 29;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
return SizedBox( return Container(
decoration: BoxDecoration(
color: theme.colorScheme.primary,
borderRadius: BorderRadius.circular(4),
),
width: boxSize, width: boxSize,
height: boxSize, height: boxSize,
child: Center( child: Center(
child: IconButton( child: IconButton(
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
icon: Icon( icon: const Icon(
Icons.add, Icons.add,
color: theme.primaryColor, color: Colors.white,
size: 20, size: 20,
), ),
onPressed: () => onAddToCart(product), onPressed: () => onAddToCart(product),
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.secondary,
),
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
),
), ),
), ),
); );

View file

@ -1,6 +1,6 @@
import "package:cached_network_image/cached_network_image.dart"; import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart"; import "package:flutter_shopping/flutter_shopping.dart";
/// A widget that displays a weekly discount. /// A widget that displays a weekly discount.
class WeeklyDiscount extends StatelessWidget { class WeeklyDiscount extends StatelessWidget {
@ -15,10 +15,10 @@ class WeeklyDiscount extends StatelessWidget {
final ProductPageConfiguration configuration; final ProductPageConfiguration configuration;
/// The product for which the discount is displayed. /// The product for which the discount is displayed.
final ProductPageProduct product; final Product product;
/// The top padding of the widget. /// The top padding of the widget.
static const double topPadding = 32.0; static const double topPadding = 20;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -28,9 +28,7 @@ class WeeklyDiscount extends StatelessWidget {
padding: const EdgeInsets.all(20.0), padding: const EdgeInsets.all(20.0),
child: Text( child: Text(
configuration.getDiscountDescription!(product), configuration.getDiscountDescription!(product),
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.bodyMedium,
color: theme.colorScheme.primary,
),
textAlign: TextAlign.left, textAlign: TextAlign.left,
), ),
); );
@ -73,9 +71,9 @@ class WeeklyDiscount extends StatelessWidget {
); );
var topText = DecoratedBox( var topText = DecoratedBox(
decoration: BoxDecoration( decoration: const BoxDecoration(
color: theme.primaryColor, color: Colors.black,
borderRadius: const BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(4), topLeft: Radius.circular(4),
topRight: Radius.circular(4), topRight: Radius.circular(4),
), ),
@ -88,10 +86,8 @@ class WeeklyDiscount extends StatelessWidget {
horizontal: 16, horizontal: 16,
), ),
child: Text( child: Text(
configuration.localizations.discountTitle.toUpperCase(), configuration.localizations.discountTitle,
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.headlineSmall,
color: theme.colorScheme.onPrimary,
),
textAlign: TextAlign.left, textAlign: TextAlign.left,
), ),
), ),
@ -100,7 +96,6 @@ class WeeklyDiscount extends StatelessWidget {
var boxDecoration = BoxDecoration( var boxDecoration = BoxDecoration(
border: Border.all( border: Border.all(
color: theme.primaryColor,
width: 1.0, width: 1.0,
), ),
borderRadius: BorderRadius.circular(4.0), borderRadius: BorderRadius.circular(4.0),

View file

@ -121,6 +121,7 @@ class _ProductPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var pageContent = SingleChildScrollView( var pageContent = SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
ShopSelector( ShopSelector(
configuration: configuration, configuration: configuration,
@ -142,11 +143,10 @@ class _ProductPage extends StatelessWidget {
pageContent, pageContent,
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: configuration.navigateToShoppingCartBuilder != null child: configuration.navigateToShoppingCartBuilder(
? configuration.navigateToShoppingCartBuilder!(context) context,
: _NavigateToShoppingCartButton( configuration,
configuration: configuration, shoppingCartNotifier,
shoppingCartNotifier: shoppingCartNotifier,
), ),
), ),
], ],
@ -154,55 +154,6 @@ class _ProductPage extends StatelessWidget {
} }
} }
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 { class _ShopContents extends StatelessWidget {
const _ShopContents({ const _ShopContents({
required this.configuration, required this.configuration,
@ -215,7 +166,9 @@ class _ShopContents extends StatelessWidget {
final ShoppingCartNotifier shoppingCartNotifier; final ShoppingCartNotifier shoppingCartNotifier;
@override @override
Widget build(BuildContext context) => Padding( Widget build(BuildContext context) {
var theme = Theme.of(context);
return Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: configuration.pagePadding.horizontal, horizontal: configuration.pagePadding.horizontal,
), ),
@ -248,7 +201,7 @@ class _ShopContents extends StatelessWidget {
} }
var productList = Padding( var productList = Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8), padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Column( child: Column(
children: [ children: [
// Products // Products
@ -267,6 +220,7 @@ class _ShopContents extends StatelessWidget {
); );
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Discounted product // Discounted product
if (productPageContent.discountedProduct != null) ...[ if (productPageContent.discountedProduct != null) ...[
@ -278,6 +232,15 @@ class _ShopContents extends StatelessWidget {
), ),
), ),
], ],
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Text(
"What would you like to order?",
style: theme.textTheme.titleLarge,
textAlign: TextAlign.start,
),
),
productList, productList,
], ],
@ -285,4 +248,5 @@ class _ShopContents extends StatelessWidget {
}, },
), ),
); );
}
} }

View file

@ -24,13 +24,13 @@ class ProductPageScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
appBar: configuration.appBar!.call(context),
body: SafeArea( body: SafeArea(
child: ProductPage( child: ProductPage(
configuration: configuration, configuration: configuration,
initialBuildShopId: initialBuildShopId, initialBuildShopId: initialBuildShopId,
), ),
), ),
appBar: configuration.appBar,
bottomNavigationBar: configuration.bottomNavigationBar, bottomNavigationBar: configuration.bottomNavigationBar,
); );
} }

View file

@ -9,7 +9,7 @@ class HorizontalListItems extends StatelessWidget {
required this.selectedItem, required this.selectedItem,
required this.onTap, required this.onTap,
this.paddingBetweenButtons = 2.0, this.paddingBetweenButtons = 2.0,
this.paddingOnButtons = 4, this.paddingOnButtons = 6,
super.key, super.key,
}); });
@ -32,7 +32,11 @@ class HorizontalListItems extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
return SingleChildScrollView( return Padding(
padding: const EdgeInsets.only(
top: 4,
),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
children: shops children: shops
@ -45,7 +49,7 @@ class HorizontalListItems extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: shop.id == selectedItem color: shop.id == selectedItem
? theme.colorScheme.primary ? theme.colorScheme.primary
: theme.colorScheme.secondary, : Colors.white,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
border: Border.all( border: Border.all(
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
@ -68,6 +72,7 @@ class HorizontalListItems extends StatelessWidget {
) )
.toList(), .toList(),
), ),
),
); );
} }
} }

View file

@ -1,5 +1,5 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart"; import "package:flutter_shopping/flutter_shopping.dart";
/// A popup that displays the product item. /// A popup that displays the product item.
class ProductItemPopup extends StatelessWidget { class ProductItemPopup extends StatelessWidget {
@ -11,7 +11,7 @@ class ProductItemPopup extends StatelessWidget {
}); });
/// The product to display. /// The product to display.
final ProductPageProduct product; final Product product;
/// Configuration for the product page. /// Configuration for the product page.
final ProductPageConfiguration configuration; final ProductPageConfiguration configuration;
@ -20,50 +20,46 @@ class ProductItemPopup extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
var productDescription = Padding( return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(44, 32, 44, 20), child: Padding(
child: Text( padding: const EdgeInsets.all(32),
product.name, child: SizedBox(
width: double.infinity,
child: Column(
children: [
Text(
product.description,
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
); Padding(
padding: const EdgeInsets.only(top: 20, left: 40, right: 40),
var closeButton = Padding(
padding: const EdgeInsets.fromLTRB(80, 0, 80, 32),
child: SizedBox( child: SizedBox(
width: 254, width: double.infinity,
child: ElevatedButton( child: FilledButton(
style: theme.elevatedButtonTheme.style?.copyWith(
shape: WidgetStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
),
child: Padding( child: Padding(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
child: Text( child: Text(
configuration.localizations.close, configuration.localizations.close,
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.displayLarge,
color: theme.colorScheme.onSurface,
), ),
), ),
), ),
), ),
), ),
);
return SingleChildScrollView(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
productDescription,
closeButton,
], ],
), ),
), ),
),
); );
} }
} }

View file

@ -33,118 +33,44 @@ class SpacedWrap extends StatelessWidget {
/// Callback when an item is tapped. /// Callback when an item is tapped.
final Function(ProductPageShop shop) onTap; final Function(ProductPageShop shop) onTap;
Row _buildRow( @override
BuildContext context, Widget build(BuildContext context) {
List<int> currentRow,
double availableRowLength,
) {
var theme = Theme.of(context); var theme = Theme.of(context);
return Wrap(
var row = <Widget>[]; alignment: WrapAlignment.center,
var extraButtonPadding = availableRowLength / currentRow.length / 2; spacing: 4,
children: [
for (var i = 0, len = currentRow.length; i < len; i++) { for (var shop in shops) ...[
var shop = shops[currentRow[i]];
row.add(
Padding( Padding(
padding: EdgeInsets.only(top: paddingBetweenButtons), padding: EdgeInsets.only(top: paddingBetweenButtons),
child: InkWell( child: InkWell(
onTap: () => onTap(shop), onTap: () => onTap(shop),
child: Container( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: shop.id == selectedItem color: shop.id == selectedItem
? theme.colorScheme.primary ? Theme.of(context).colorScheme.primary
: theme.colorScheme.secondary, : Colors.white,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
border: Border.all( border: Border.all(
color: theme.colorScheme.primary, color: Theme.of(context).colorScheme.primary,
width: 1, width: 1,
), ),
), ),
padding: EdgeInsets.symmetric( child: Padding(
horizontal: paddingOnButtons + extraButtonPadding, padding: const EdgeInsets.all(8.0),
vertical: paddingOnButtons,
),
child: Text( child: Text(
shop.name, shop.name,
style: shop.id == selectedItem style: shop.id == selectedItem
? theme.textTheme.bodyMedium?.copyWith( ? theme.textTheme.titleMedium
color: Colors.white, ?.copyWith(color: Colors.white)
fontWeight: FontWeight.bold,
)
: theme.textTheme.bodyMedium, : 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

@ -1,7 +1,7 @@
name: flutter_product_page name: flutter_product_page
description: "A Flutter module for the product page" description: "A Flutter module for the product page"
publish_to: 'none' publish_to: 'none'
version: 1.0.0 version: 2.0.0
environment: environment:
sdk: '>=3.3.4 <4.0.0' sdk: '>=3.3.4 <4.0.0'
@ -15,6 +15,11 @@ dependencies:
git: git:
url: https://github.com/Iconica-Development/flutter_nested_categories url: https://github.com/Iconica-Development/flutter_nested_categories
ref: 0.0.1 ref: 0.0.1
flutter_shopping:
git:
url: https://github.com/Iconica-Development/flutter_shopping
path: packages/flutter_shopping
ref: 2.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -26,13 +31,3 @@ dev_dependencies:
flutter: flutter:
uses-material-design: true 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,4 +1,3 @@
import "package:example/src/models/my_product.dart";
import "package:example/src/routes.dart"; import "package:example/src/routes.dart";
import "package:example/src/services/order_service.dart"; import "package:example/src/services/order_service.dart";
import "package:example/src/services/shop_service.dart"; import "package:example/src/services/shop_service.dart";
@ -7,7 +6,7 @@ import "package:flutter_shopping/flutter_shopping.dart";
import "package:go_router/go_router.dart"; import "package:go_router/go_router.dart";
// (REQUIRED): Create your own instance of the ProductService. // (REQUIRED): Create your own instance of the ProductService.
final ProductService<MyProduct> productService = ProductService([]); final ProductService<Product> productService = ProductService([]);
FlutterShoppingConfiguration getFlutterShoppingConfiguration() => FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
FlutterShoppingConfiguration( FlutterShoppingConfiguration(
@ -24,8 +23,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
shops: Future.value(getShops()), shops: Future.value(getShops()),
// (REQUIRED): Function to add a product to the cart // (REQUIRED): Function to add a product to the cart
onAddToCart: (ProductPageProduct product) => onAddToCart: productService.addProduct,
productService.addProduct(product as MyProduct),
// (REQUIRED): Function to get the products for a shop // (REQUIRED): Function to get the products for a shop
getProducts: (ProductPageShop shop) => getProducts: (ProductPageShop shop) =>
@ -34,7 +32,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
), ),
// (REQUIRED): Function to navigate to the shopping cart // (REQUIRED): Function to navigate to the shopping cart
onNavigateToShoppingCart: () => onCompleteProductPage(context), onNavigateToShoppingCart: () async => onCompleteProductPage(context),
// (RECOMMENDED): Function to get the number of products in the // (RECOMMENDED): Function to get the number of products in the
// shopping cart. This is used to display the number of products // shopping cart. This is used to display the number of products
@ -43,7 +41,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
// (RECOMMENDED) Function that returns the description for a // (RECOMMENDED) Function that returns the description for a
// product that is on sale. // product that is on sale.
getDiscountDescription: (ProductPageProduct product) => getDiscountDescription: (product) =>
"""${product.name} for just \$${product.discountPrice?.toStringAsFixed(2)}""", """${product.name} for just \$${product.discountPrice?.toStringAsFixed(2)}""",
// (RECOMMENDED) Function that is fired when the shop selection // (RECOMMENDED) Function that is fired when the shop selection
@ -60,7 +58,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
localizations: const ProductPageLocalization(), localizations: const ProductPageLocalization(),
// (OPTIONAL) Appbar // (OPTIONAL) Appbar
appBar: AppBar( appBar: (context) => AppBar(
title: const Text("Shop"), title: const Text("Shop"),
leading: IconButton( leading: IconButton(
icon: const Icon( icon: const Icon(
@ -85,7 +83,8 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
productService: productService, productService: productService,
// (REQUIRED) product item builder: // (REQUIRED) product item builder:
productItemBuilder: (context, locale, product) => ListTile( productItemBuilder: (context, locale, product, service, config) =>
ListTile(
title: Text(product.name), title: Text(product.name),
subtitle: Text(product.price.toStringAsFixed(2)), subtitle: Text(product.price.toStringAsFixed(2)),
leading: Image.network( leading: Image.network(
@ -116,14 +115,11 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
// (OPTIONAL/REQUIRED) on confirm order callback: // (OPTIONAL/REQUIRED) on confirm order callback:
// Either use this callback or the placeOrderButtonBuilder. // Either use this callback or the placeOrderButtonBuilder.
onConfirmOrder: (products) => onCompleteShoppingCart(context), onConfirmOrder: (products) async => onCompleteShoppingCart(context),
// (RECOMMENDED) localizations: // (RECOMMENDED) localizations:
localizations: const ShoppingCartLocalizations(), localizations: const ShoppingCartLocalizations(),
// (OPTIONAL) title above product list:
title: "Products",
/// (OPTIONAL) no content builder for when there are no products /// (OPTIONAL) no content builder for when there are no products
/// in the shopping cart. /// in the shopping cart.
noContentBuilder: (context) => const Center( noContentBuilder: (context) => const Center(

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,7 +1,6 @@
import "package:example/src/models/my_product.dart";
import "package:flutter_shopping/flutter_shopping.dart"; import "package:flutter_shopping/flutter_shopping.dart";
/// Example implementation of storing an order in a database. /// Example implementation of storing an order in a database.
void storeOrderInDatabase(List<MyProduct> products, OrderResult result) { void storeOrderInDatabase(List<Product> products, OrderResult result) {
return; return;
} }

View file

@ -1,4 +1,3 @@
import "package:example/src/models/my_product.dart";
import "package:example/src/models/my_shop.dart"; import "package:example/src/models/my_shop.dart";
import "package:flutter_shopping/flutter_shopping.dart"; import "package:flutter_shopping/flutter_shopping.dart";
@ -20,8 +19,8 @@ ProductPageContent getShopContent(String shopId) {
/// This function should have your own implementation. Generally this would /// This function should have your own implementation. Generally this would
/// contain some API call to fetch the list of products for a shop. /// contain some API call to fetch the list of products for a shop.
List<MyProduct> getProducts(String shopId) => <MyProduct>[ List<Product> getProducts(String shopId) => <Product>[
MyProduct( Product(
id: "1", id: "1",
name: "White bread", name: "White bread",
price: 2.99, price: 2.99,
@ -29,19 +28,22 @@ List<MyProduct> getProducts(String shopId) => <MyProduct>[
imageUrl: "https://via.placeholder.com/150", imageUrl: "https://via.placeholder.com/150",
hasDiscount: true, hasDiscount: true,
discountPrice: 1.99, discountPrice: 1.99,
description: "",
), ),
MyProduct( Product(
id: "2", id: "2",
name: "Brown bread", name: "Brown bread",
price: 2.99, price: 2.99,
category: "Loaves", category: "Loaves",
imageUrl: "https://via.placeholder.com/150", imageUrl: "https://via.placeholder.com/150",
description: "",
), ),
MyProduct( Product(
id: "3", id: "3",
name: "Cheese sandwich", name: "Cheese sandwich",
price: 1.99, price: 1.99,
category: "Sandwiches", category: "Sandwiches",
imageUrl: "https://via.placeholder.com/150", imageUrl: "https://via.placeholder.com/150",
description: "",
), ),
]; ];

View file

@ -12,12 +12,11 @@ dependencies:
flutter_hooks: ^0.20.0 flutter_hooks: ^0.20.0
hooks_riverpod: ^2.1.1 hooks_riverpod: ^2.1.1
go_router: 12.1.3 go_router: 12.1.3
# Iconica packages
## Userstories
flutter_shopping: flutter_shopping:
path: ../ git:
url: https://github.com/Iconica-Development/flutter_shopping
path: packages/flutter_shopping
ref: 2.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -1,4 +1,3 @@
import "package:amazon/src/models/my_product.dart";
import "package:amazon/src/routes.dart"; import "package:amazon/src/routes.dart";
import "package:amazon/src/services/category_service.dart"; import "package:amazon/src/services/category_service.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
@ -6,7 +5,7 @@ import "package:flutter_shopping/flutter_shopping.dart";
import "package:go_router/go_router.dart"; import "package:go_router/go_router.dart";
// (REQUIRED): Create your own instance of the ProductService. // (REQUIRED): Create your own instance of the ProductService.
final ProductService<MyProduct> productService = ProductService([]); final ProductService<Product> productService = ProductService([]);
FlutterShoppingConfiguration getFlutterShoppingConfiguration() => FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
FlutterShoppingConfiguration( FlutterShoppingConfiguration(
@ -27,8 +26,9 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
pagePadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), pagePadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4),
// (REQUIRED): Function to add a product to the cart // (REQUIRED): Function to add a product to the cart
onAddToCart: (ProductPageProduct product) => onAddToCart: (product) {
productService.addProduct(product as MyProduct), return productService.addProduct(product);
},
// (REQUIRED): Function to get the products for a shop // (REQUIRED): Function to get the products for a shop
getProducts: (ProductPageShop shop) => getProducts: (ProductPageShop shop) =>
@ -41,7 +41,9 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
shopSelectorStyle: ShopSelectorStyle.row, shopSelectorStyle: ShopSelectorStyle.row,
navigateToShoppingCartBuilder: (context) => const SizedBox.shrink(), navigateToShoppingCartBuilder: (context, productpageinfo, shop) {
return const SizedBox.shrink();
},
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
fixedColor: theme.primaryColor, fixedColor: theme.primaryColor,
@ -164,7 +166,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
const SizedBox(height: 12), const SizedBox(height: 12),
FilledButton( FilledButton(
onPressed: () { onPressed: () {
productService.addProduct(product as MyProduct); productService.addProduct(product);
}, },
child: const Text("In winkelwagen"), child: const Text("In winkelwagen"),
), ),
@ -206,7 +208,7 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
), ),
// (OPTIONAL) Appbar // (OPTIONAL) Appbar
appBar: AppBar( appBar: (context) => AppBar(
title: const SizedBox( title: const SizedBox(
height: 40, height: 40,
child: SearchBar( child: SearchBar(
@ -262,7 +264,8 @@ FlutterShoppingConfiguration getFlutterShoppingConfiguration() =>
productService: productService, productService: productService,
// (REQUIRED) product item builder: // (REQUIRED) product item builder:
productItemBuilder: (context, locale, product) => ListTile( productItemBuilder: (context, locale, product, service, config) =>
ListTile(
title: Text(product.name), title: Text(product.name),
subtitle: Text(product.price.toStringAsFixed(2)), subtitle: Text(product.price.toStringAsFixed(2)),
leading: Image.network( leading: Image.network(

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

View file

@ -17,7 +17,10 @@ dependencies:
url: https://github.com/Iconica-Development/flutter_nested_categories url: https://github.com/Iconica-Development/flutter_nested_categories
ref: 0.0.1 ref: 0.0.1
flutter_shopping: flutter_shopping:
path: ../ git:
url: https://github.com/Iconica-Development/flutter_shopping
path: packages/flutter_shopping
ref: 2.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -6,6 +6,7 @@ export "package:flutter_product_page/flutter_product_page.dart";
export "package:flutter_shopping_cart/flutter_shopping_cart.dart"; export "package:flutter_shopping_cart/flutter_shopping_cart.dart";
export "src/config/flutter_shopping_configuration.dart"; export "src/config/flutter_shopping_configuration.dart";
export "src/models/product.dart";
export "src/routes.dart"; export "src/routes.dart";
export "src/user_stores/flutter_shopping_userstory_go_router.dart"; export "src/user_stores/flutter_shopping_userstory_go_router.dart";
export "src/user_stores/flutter_shopping_userstory_navigation.dart"; export "src/user_stores/flutter_shopping_userstory_navigation.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

@ -0,0 +1,10 @@
/// Contains the localized strings for the order details screen.
class OrderDetailsLocalizations {
/// Creates order details localizations
OrderDetailsLocalizations({
this.orderDetailsTitle = "Information",
});
/// Title for the order details screen.
final String orderDetailsTitle;
}

View file

@ -0,0 +1,43 @@
/// The product class contains all the information that a product can have.
/// This class is used in the shopping cart and the product page.
class Product {
/// Creates a product.
Product({
required this.id,
required this.name,
required this.imageUrl,
required this.category,
required this.price,
required this.description,
this.hasDiscount = false,
this.discountPrice,
this.quantity = 1,
});
/// The unique identifier for the product.
final String id;
/// The name of the product.
final String name;
/// The image URL of the product.
final String imageUrl;
/// The category of the product.
final String category;
/// The price of the product.
final double price;
/// Whether the product has a discount or not.
final bool hasDiscount;
/// The discounted price of the product. Only used if [hasDiscount] is true.
final double? discountPrice;
/// Quantity for the product.
int quantity;
/// The description of the product.
final String description;
}

View file

@ -1,5 +1,4 @@
import "package:flutter_shopping/flutter_shopping.dart"; 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_failed_widget.dart";
import "package:flutter_shopping/src/widgets/default_order_succes_widget.dart"; import "package:flutter_shopping/src/widgets/default_order_succes_widget.dart";
import "package:go_router/go_router.dart"; import "package:go_router/go_router.dart";
@ -29,8 +28,11 @@ List<GoRoute> getShoppingStoryRoutes({
builder: (context, state) => configuration.orderDetailsBuilder != null builder: (context, state) => configuration.orderDetailsBuilder != null
? configuration.orderDetailsBuilder!(context) ? configuration.orderDetailsBuilder!(context)
: OrderDetailScreen( : OrderDetailScreen(
configuration: configuration: OrderDetailConfiguration(
getDefaultOrderDetailConfiguration(context, configuration), onCompleted: (result) {
context.go(FlutterShoppingPathRoutes.orderSuccess);
},
),
), ),
), ),
GoRoute( GoRoute(

View file

@ -35,18 +35,18 @@ Future<void> onCompleteOrderDetails(
/// ///
/// You can create your own implementation if you decide to use a different /// You can create your own implementation if you decide to use a different
/// approach. /// approach.
void onCompleteShoppingCart( Future<void> onCompleteShoppingCart(
BuildContext context, BuildContext context,
) { ) async {
context.go(FlutterShoppingPathRoutes.orderDetails); await context.push(FlutterShoppingPathRoutes.orderDetails);
} }
/// Default on complete product page function. /// Default on complete product page function.
/// ///
/// You can create your own implementation if you decide to use a different /// You can create your own implementation if you decide to use a different
/// approach. /// approach.
void onCompleteProductPage( Future<void> onCompleteProductPage(
BuildContext context, BuildContext context,
) { ) async {
context.go(FlutterShoppingPathRoutes.shoppingCart); await context.push(FlutterShoppingPathRoutes.shoppingCart);
} }

View file

@ -16,51 +16,186 @@ class DefaultOrderSucces extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
var theme = Theme.of(context); var theme = Theme.of(context);
var finishOrderButton = FilledButton( return Scaffold(
onPressed: () => configuration.onCompleteUserStory(context), 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 Peter for your order!",
style: theme.textTheme.bodyMedium,
),
const SizedBox(
height: 16,
),
Text(
"The order was placed at Bakkerij de Goudkorst."
" You can pick this"
" up on Monday, February 7 at 1:00 PM.",
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(
height: 16,
),
Text(
"If you want, you can place another order in this street.",
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
const SizedBox(
height: 32,
),
Text(
"Weekly offers",
style: theme.textTheme.headlineSmall
?.copyWith(color: Colors.black),
),
const SizedBox(
height: 4,
),
],
),
),
SizedBox(
height: 272,
child: ListView(
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children: [
const SizedBox(width: 32),
_discount(context),
const SizedBox(width: 8),
_discount(context),
const SizedBox(width: 32),
],
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () async {
configuration.onCompleteUserStory.call(context);
},
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 32.0, horizontal: 16.0,
vertical: 8.0, vertical: 12,
), ),
child: Text("Finish Order".toUpperCase()), child: Text(
"Place another order",
style: theme.textTheme.displayLarge,
), ),
);
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,
), ),
), ),
); );
} }
} }
Widget _discount(BuildContext context) {
var theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.black,
),
borderRadius: BorderRadius.circular(10),
),
width: MediaQuery.of(context).size.width - 64,
height: 200,
child: Stack(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10),
),
child: Image.network(
"https://picsum.photos/150",
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
),
Container(
alignment: Alignment.centerLeft,
height: 38,
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
),
),
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
"Butcher Puurvlees",
style: theme.textTheme.headlineSmall?.copyWith(
color: Colors.white,
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
alignment: Alignment.centerLeft,
width: MediaQuery.of(context).size.width,
height: 68,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
"Chicken legs, now for 4,99",
style: theme.textTheme.bodyMedium,
),
),
),
),
],
),
);
}

View file

@ -1,7 +1,7 @@
name: flutter_shopping name: flutter_shopping
description: "A new Flutter project." description: "A new Flutter project."
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: 'none'
version: 1.0.0 version: 2.0.0
environment: environment:
sdk: '>=3.3.4 <4.0.0' sdk: '>=3.3.4 <4.0.0'
@ -13,17 +13,17 @@ dependencies:
flutter_product_page: flutter_product_page:
git: git:
url: https://github.com/Iconica-Development/flutter_shopping url: https://github.com/Iconica-Development/flutter_shopping
ref: 1.0.0 ref: 2.0.0
path: packages/flutter_product_page path: packages/flutter_product_page
flutter_shopping_cart: flutter_shopping_cart:
git: git:
url: https://github.com/Iconica-Development/flutter_shopping url: https://github.com/Iconica-Development/flutter_shopping
ref: 1.0.0 ref: 2.0.0
path: packages/flutter_shopping_cart path: packages/flutter_shopping_cart
flutter_order_details: flutter_order_details:
git: git:
url: https://github.com/Iconica-Development/flutter_shopping url: https://github.com/Iconica-Development/flutter_shopping
ref: 1.0.0 ref: 2.0.0
path: packages/flutter_order_details path: packages/flutter_order_details
dev_dependencies: dev_dependencies:
@ -35,13 +35,4 @@ dev_dependencies:
ref: 7.0.0 ref: 7.0.0
flutter: 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

@ -3,6 +3,5 @@ library flutter_shopping_cart;
export "src/config/shopping_cart_config.dart"; export "src/config/shopping_cart_config.dart";
export "src/config/shopping_cart_localizations.dart"; export "src/config/shopping_cart_localizations.dart";
export "src/models/shopping_cart_product.dart";
export "src/services/product_service.dart"; export "src/services/product_service.dart";
export "src/widgets/shopping_cart_screen.dart"; export "src/widgets/shopping_cart_screen.dart";

View file

@ -1,5 +1,6 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_shopping_cart/src/widgets/product_item_popup.dart";
Widget _defaultNoContentBuilder(BuildContext context) => Widget _defaultNoContentBuilder(BuildContext context) =>
const SizedBox.shrink(); const SizedBox.shrink();
@ -7,30 +8,21 @@ Widget _defaultNoContentBuilder(BuildContext context) =>
/// Shopping cart configuration /// Shopping cart configuration
/// ///
/// This class is used to configure the shopping cart. /// This class is used to configure the shopping cart.
class ShoppingCartConfig<T extends ShoppingCartProduct> { class ShoppingCartConfig<T extends Product> {
/// Creates a shopping cart configuration. /// Creates a shopping cart configuration.
ShoppingCartConfig({ ShoppingCartConfig({
required this.productService, required this.productService,
// this.productItemBuilder = _defaultProductItemBuilder,
this.onConfirmOrder, this.onConfirmOrder,
this.confirmOrderButtonBuilder, this.confirmOrderButtonBuilder,
this.confirmOrderButtonHeight = 100, this.confirmOrderButtonHeight = 100,
//
this.sumBottomSheetBuilder, this.sumBottomSheetBuilder,
this.sumBottomSheetHeight = 100, this.sumBottomSheetHeight = 100,
//
this.title,
this.titleBuilder, this.titleBuilder,
//
this.localizations = const ShoppingCartLocalizations(), this.localizations = const ShoppingCartLocalizations(),
//
this.padding = const EdgeInsets.symmetric(horizontal: 32), this.padding = const EdgeInsets.symmetric(horizontal: 32),
this.bottomPadding = const EdgeInsets.fromLTRB(44, 0, 44, 32), this.bottomPadding = const EdgeInsets.fromLTRB(44, 0, 44, 32),
//
this.appBar, this.appBar,
//
Widget Function(BuildContext context, Locale locale, T product)?
productItemBuilder,
Widget Function(BuildContext context) noContentBuilder = Widget Function(BuildContext context) noContentBuilder =
_defaultNoContentBuilder, _defaultNoContentBuilder,
}) : assert( }) : assert(
@ -45,17 +37,7 @@ you cannot use the onConfirmOrder callback.""",
If you do not override the confirm order button builder, If you do not override the confirm order button builder,
you must use the onConfirmOrder callback.""", you must use the onConfirmOrder callback.""",
), ),
_noContentBuilder = noContentBuilder { _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),
),
);
}
/// Product Service. The service contains all the products that /// Product Service. The service contains all the products that
/// a shopping cart can contain. Each product must extend the [Product] class. /// a shopping cart can contain. Each product must extend the [Product] class.
@ -65,13 +47,15 @@ you must use the onConfirmOrder callback.""",
/// support seperate shopping carts for shop. /// support seperate shopping carts for shop.
ProductService<T> productService = ProductService<T>(<T>[]); ProductService<T> productService = ProductService<T>(<T>[]);
late final Widget Function(BuildContext context, Locale locale, T product)?
_productItemBuilder;
/// Product item builder. This builder is used to build the product item /// Product item builder. This builder is used to build the product item
/// that will be displayed in the shopping cart. /// that will be displayed in the shopping cart.
Widget Function(BuildContext context, Locale locale, T product) final Widget Function(
get productItemBuilder => _productItemBuilder!; BuildContext context,
Locale locale,
Product product,
ProductService<Product> productService,
ShoppingCartConfig configuration,
) productItemBuilder;
final Widget Function(BuildContext context) _noContentBuilder; final Widget Function(BuildContext context) _noContentBuilder;
@ -115,10 +99,6 @@ you must use the onConfirmOrder callback.""",
/// [sumBottomSheetBuilder] is overridden. /// [sumBottomSheetBuilder] is overridden.
final EdgeInsets bottomPadding; final EdgeInsets bottomPadding;
/// Title of the shopping cart. The title is displayed at the top of the
/// shopping cart. If you provide a title builder, the title will be ignored.
final String? title;
/// Title builder. This builder is used to build the title of the shopping /// 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 /// cart. The title is displayed at the top of the shopping cart. If you
/// use the title builder, the [title] will be ignored. /// use the title builder, the [title] will be ignored.
@ -131,3 +111,99 @@ you must use the onConfirmOrder callback.""",
/// App bar for the shopping cart screen. /// App bar for the shopping cart screen.
final PreferredSizeWidget? appBar; final PreferredSizeWidget? appBar;
} }
Widget _defaultProductItemBuilder(
BuildContext context,
Locale locale,
Product product,
ProductService<Product> service,
ShoppingCartConfig configuration,
) {
var theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: ListTile(
contentPadding: const EdgeInsets.only(top: 3, left: 4, bottom: 3),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: theme.textTheme.titleMedium,
),
IconButton(
onPressed: () async {
await showModalBottomSheet(
context: context,
backgroundColor: theme.colorScheme.surface,
builder: (context) => ProductItemPopup(
product: product,
configuration: configuration,
),
);
},
icon: Icon(
Icons.info_outline,
color: theme.colorScheme.primary,
),
),
],
),
leading: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: Image.network(
product.imageUrl,
),
),
trailing: Column(
children: [
Text(
product.price.toStringAsFixed(2),
style: theme.textTheme.labelSmall,
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
constraints: const BoxConstraints(),
padding: EdgeInsets.zero,
icon: const Icon(
Icons.remove,
color: Colors.black,
),
onPressed: () => service.removeOneProduct(product),
),
Padding(
padding: const EdgeInsets.all(2),
child: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: theme.colorScheme.primary,
borderRadius: BorderRadius.circular(4),
),
height: 30,
width: 30,
child: Text(
"${product.quantity}",
style: theme.textTheme.titleSmall,
textAlign: TextAlign.center,
),
),
),
IconButton(
constraints: const BoxConstraints(),
padding: EdgeInsets.zero,
icon: const Icon(
Icons.add,
color: Colors.black,
),
onPressed: () => service.addProduct(product),
),
],
),
],
),
),
);
}

View file

@ -5,8 +5,10 @@ class ShoppingCartLocalizations {
/// Creates shopping cart localizations /// Creates shopping cart localizations
const ShoppingCartLocalizations({ const ShoppingCartLocalizations({
this.locale = const Locale("en", "US"), this.locale = const Locale("en", "US"),
this.placeOrder = "PLACE ORDER", this.placeOrder = "Order",
this.sum = "Total:", this.sum = "Subtotal:",
this.cartTitle = "Products",
this.close = "close",
}); });
/// Locale for the shopping cart. /// Locale for the shopping cart.
@ -21,4 +23,11 @@ class ShoppingCartLocalizations {
/// Localization for the sum. /// Localization for the sum.
final String 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,9 +1,9 @@
import "package:flutter/foundation.dart"; import "package:flutter/foundation.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; import "package:flutter_shopping/flutter_shopping.dart";
/// Product service. This class is responsible for managing the products. /// Product service. This class is responsible for managing the products.
/// The service is used to add, remove, and update products. /// The service is used to add, remove, and update products.
class ProductService<T extends ShoppingCartProduct> extends ChangeNotifier { class ProductService<T extends Product> extends ChangeNotifier {
/// Creates a product service. /// Creates a product service.
ProductService(this.products); ProductService(this.products);

View file

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

View file

@ -1,9 +1,8 @@
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart"; import "package:flutter_shopping/flutter_shopping.dart";
/// Shopping cart screen widget. /// Shopping cart screen widget.
class ShoppingCartScreen<T extends ShoppingCartProduct> class ShoppingCartScreen<T extends Product> extends StatelessWidget {
extends StatelessWidget {
/// Creates a shopping cart screen. /// Creates a shopping cart screen.
const ShoppingCartScreen({ const ShoppingCartScreen({
required this.configuration, required this.configuration,
@ -22,12 +21,19 @@ class ShoppingCartScreen<T extends ShoppingCartProduct>
children: [ children: [
if (configuration.titleBuilder != null) ...{ if (configuration.titleBuilder != null) ...{
configuration.titleBuilder!(context), configuration.titleBuilder!(context),
} else if (configuration.title != null) ...{ } else ...{
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), padding: const EdgeInsets.symmetric(
child: Text( vertical: 32,
configuration.title!, ),
child: Row(
children: [
Text(
configuration.localizations.cartTitle,
style: theme.textTheme.titleLarge, style: theme.textTheme.titleLarge,
textAlign: TextAlign.start,
),
],
), ),
), ),
}, },
@ -47,6 +53,8 @@ class ShoppingCartScreen<T extends ShoppingCartProduct>
context, context,
configuration.localizations.locale, configuration.localizations.locale,
product, product,
configuration.productService,
configuration,
), ),
// Additional whitespace at the bottom to make sure the // Additional whitespace at the bottom to make sure the
// last product(s) are not hidden by the bottom sheet. // last product(s) are not hidden by the bottom sheet.
@ -62,41 +70,22 @@ class ShoppingCartScreen<T extends ShoppingCartProduct>
), ),
); );
var bottomHeight = configuration.confirmOrderButtonHeight +
configuration.sumBottomSheetHeight;
var bottomBlur = Container(
height: bottomHeight,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
theme.colorScheme.surface.withOpacity(0),
theme.colorScheme.surface.withOpacity(.5),
theme.colorScheme.surface.withOpacity(.8),
theme.colorScheme.surface.withOpacity(.8),
theme.colorScheme.surface.withOpacity(.8),
theme.colorScheme.surface.withOpacity(.8),
theme.colorScheme.surface.withOpacity(1),
],
),
),
);
return Scaffold( return Scaffold(
appBar: configuration.appBar, appBar: configuration.appBar ??
body: Stack( AppBar(
title: Text(
"Shopping cart",
style: theme.textTheme.headlineLarge,
),
),
body: SafeArea(
child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
Padding( Padding(
padding: configuration.padding, padding: configuration.padding,
child: productBuilder, child: productBuilder,
), ),
Align(
alignment: Alignment.bottomCenter,
child: bottomBlur,
),
Align( Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: _BottomSheet<T>( child: _BottomSheet<T>(
@ -105,11 +94,12 @@ class ShoppingCartScreen<T extends ShoppingCartProduct>
), ),
], ],
), ),
),
); );
} }
} }
class _BottomSheet<T extends ShoppingCartProduct> extends StatelessWidget { class _BottomSheet<T extends Product> extends StatelessWidget {
const _BottomSheet({ const _BottomSheet({
required this.configuration, required this.configuration,
super.key, super.key,
@ -145,8 +135,7 @@ class _BottomSheet<T extends ShoppingCartProduct> extends StatelessWidget {
} }
} }
class _DefaultConfirmOrderButton<T extends ShoppingCartProduct> class _DefaultConfirmOrderButton<T extends Product> extends StatelessWidget {
extends StatelessWidget {
const _DefaultConfirmOrderButton({ const _DefaultConfirmOrderButton({
required this.configuration, required this.configuration,
}); });
@ -169,26 +158,27 @@ class _DefaultConfirmOrderButton<T extends ShoppingCartProduct>
configuration.onConfirmOrder!(products); configuration.onConfirmOrder!(products);
} }
return SafeArea( return Padding(
child: Padding( padding: const EdgeInsets.symmetric(horizontal: 60),
padding: const EdgeInsets.symmetric(horizontal: 80), child: SizedBox(
child: ElevatedButton( width: double.infinity,
style: ElevatedButton.styleFrom( child: FilledButton(
backgroundColor: theme.colorScheme.primary,
),
onPressed: () => onConfirmOrderPressed( onPressed: () => onConfirmOrderPressed(
configuration.productService.products, configuration.productService.products,
), ),
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16.0, horizontal: 16.0,
vertical: 8.0, vertical: 12,
), ),
child: Text( child: Text(
"""${configuration.localizations.placeOrder} (${configuration.productService.countProducts()})""", configuration.localizations.placeOrder,
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.displayLarge,
color: theme.colorScheme.onPrimary,
),
), ),
), ),
), ),
@ -222,8 +212,8 @@ class _DefaultSumBottomSheet extends StatelessWidget {
), ),
const Spacer(), const Spacer(),
Text( Text(
totalPrice.toStringAsFixed(2), "${totalPrice.toStringAsFixed(2)}",
style: theme.textTheme.titleMedium, style: theme.textTheme.bodyMedium,
), ),
], ],
), ),

View file

@ -1,6 +1,7 @@
name: flutter_shopping_cart name: flutter_shopping_cart
description: "A Flutter module for a shopping cart." description: "A Flutter module for a shopping cart."
version: 1.0.0 version: 2.0.0
publish_to: 'none'
environment: environment:
sdk: '>=3.3.0 <4.0.0' sdk: '>=3.3.0 <4.0.0'
@ -9,6 +10,12 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_shopping:
git:
url: https://github.com/Iconica-Development/flutter_shopping
path: packages/flutter_shopping
ref: 2.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: