mirror of
https://github.com/Iconica-Development/flutter_shopping.git
synced 2025-05-19 08:53:46 +02:00
fix: order detail screens
This commit is contained in:
parent
d5309b8198
commit
4e37ec8b5d
21 changed files with 764 additions and 1545 deletions
|
@ -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";
|
||||||
|
|
|
@ -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",
|
||||||
|
"Place another order",
|
||||||
|
];
|
||||||
|
return Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 48),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: FilledButton(
|
||||||
|
onPressed: () async {
|
||||||
|
controller.validateAndSaveCurrentStep();
|
||||||
|
await controller.autoNextStep();
|
||||||
|
},
|
||||||
|
style: theme.filledButtonTheme.style?.copyWith(
|
||||||
|
backgroundColor: WidgetStateProperty.all(
|
||||||
|
theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0,
|
||||||
|
vertical: 8.0,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
nextButtonTexts[currentStep],
|
||||||
|
style: theme.textTheme.displayLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<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",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
context,
|
||||||
|
widget.configuration,
|
||||||
|
controller,
|
||||||
|
),
|
||||||
|
pages: widget.configuration.pages.call(context),
|
||||||
|
onFinished: (data) {
|
||||||
|
widget.configuration.onCompleted.call(data);
|
||||||
|
},
|
||||||
|
onNext: (step, data) {},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CurrentStep extends ChangeNotifier {
|
|
||||||
int _step = 0;
|
|
||||||
|
|
||||||
int get step => _step;
|
|
||||||
|
|
||||||
void increment() {
|
|
||||||
_step++;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
void decrement() {
|
|
||||||
_step--;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _OrderDetailBody extends StatelessWidget {
|
|
||||||
const _OrderDetailBody({
|
|
||||||
required this.configuration,
|
|
||||||
required this.orderResult,
|
|
||||||
required this.currentStep,
|
|
||||||
required this.onBlurBackground,
|
|
||||||
});
|
|
||||||
|
|
||||||
final OrderDetailConfiguration configuration;
|
|
||||||
final OrderResult orderResult;
|
|
||||||
final _CurrentStep currentStep;
|
|
||||||
final Function({bool needsBlur}) onBlurBackground;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => ListenableBuilder(
|
|
||||||
listenable: currentStep,
|
|
||||||
builder: (context, _) => Builder(
|
|
||||||
builder: (context) => _FormBuilder(
|
|
||||||
currentStep: currentStep,
|
|
||||||
orderResult: orderResult,
|
|
||||||
configuration: configuration,
|
|
||||||
onBlurBackground: onBlurBackground,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FormBuilder extends StatelessWidget {
|
|
||||||
const _FormBuilder({
|
|
||||||
required this.currentStep,
|
|
||||||
required this.configuration,
|
|
||||||
required this.orderResult,
|
|
||||||
required this.onBlurBackground,
|
|
||||||
});
|
|
||||||
|
|
||||||
final _CurrentStep currentStep;
|
|
||||||
final OrderDetailConfiguration configuration;
|
|
||||||
final OrderResult orderResult;
|
|
||||||
|
|
||||||
final Function({bool needsBlur}) onBlurBackground;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var theme = Theme.of(context);
|
|
||||||
|
|
||||||
var progressIndicator = LinearProgressIndicator(
|
|
||||||
value: currentStep.step / configuration.steps.length,
|
|
||||||
backgroundColor: theme.colorScheme.surface,
|
|
||||||
);
|
|
||||||
|
|
||||||
var stepForm = Form(
|
|
||||||
key: configuration.steps[currentStep.step].formKey,
|
|
||||||
child: _StepBuilder(
|
|
||||||
configuration: configuration,
|
|
||||||
currentStep: configuration.steps[currentStep.step],
|
|
||||||
orderResult: orderResult,
|
|
||||||
theme: theme,
|
|
||||||
onBlurBackground: onBlurBackground,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
void onPressedNext() {
|
|
||||||
var formInfo = configuration.steps[currentStep.step];
|
|
||||||
var formkey = formInfo.formKey;
|
|
||||||
for (var input in formInfo.fields) {
|
|
||||||
orderResult.order[input.outputKey] = input.currentValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formkey.currentState!.validate()) {
|
|
||||||
currentStep.increment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void onPressedPrevious() {
|
|
||||||
var formInfo = configuration.steps[currentStep.step];
|
|
||||||
for (var input in formInfo.fields) {
|
|
||||||
orderResult.order[input.outputKey] = input.currentValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentStep.decrement();
|
|
||||||
}
|
|
||||||
|
|
||||||
void onPressedComplete() {
|
|
||||||
var formInfo = configuration.steps[currentStep.step];
|
|
||||||
var formkey = formInfo.formKey;
|
|
||||||
for (var input in formInfo.fields) {
|
|
||||||
orderResult.order[input.outputKey] = input.currentValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formkey.currentState!.validate()) {
|
|
||||||
configuration.onCompleted(orderResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var navigationControl = Row(
|
|
||||||
children: [
|
|
||||||
if (currentStep.step > 0) ...[
|
|
||||||
TextButton(
|
|
||||||
onPressed: onPressedPrevious,
|
|
||||||
child: Text(
|
|
||||||
configuration.localization.backButton,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
const Spacer(),
|
|
||||||
if (currentStep.step < configuration.steps.length - 1) ...[
|
|
||||||
TextButton(
|
|
||||||
onPressed: onPressedNext,
|
|
||||||
child: Text(
|
|
||||||
configuration.localization.nextButton,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
TextButton(
|
|
||||||
onPressed: onPressedComplete,
|
|
||||||
child: Text(
|
|
||||||
configuration.localization.completeButton,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
if (configuration.progressIndicator) ...[
|
|
||||||
progressIndicator,
|
|
||||||
],
|
|
||||||
stepForm,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: navigationControl,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StepBuilder extends StatelessWidget {
|
|
||||||
const _StepBuilder({
|
|
||||||
required this.configuration,
|
|
||||||
required this.currentStep,
|
|
||||||
required this.orderResult,
|
|
||||||
required this.theme,
|
|
||||||
required this.onBlurBackground,
|
|
||||||
});
|
|
||||||
|
|
||||||
final OrderDetailConfiguration configuration;
|
|
||||||
final OrderDetailStep currentStep;
|
|
||||||
final OrderResult orderResult;
|
|
||||||
final ThemeData theme;
|
|
||||||
final Function({bool needsBlur}) onBlurBackground;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
var title = currentStep.stepName != null
|
|
||||||
? Padding(
|
|
||||||
padding: configuration.titlePadding,
|
|
||||||
child: Text(
|
|
||||||
currentStep.stepName!,
|
|
||||||
style: theme.textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink();
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
title,
|
|
||||||
for (var input in currentStep.fields)
|
|
||||||
Padding(
|
|
||||||
padding: configuration.inputFieldPadding,
|
|
||||||
child: input.build(
|
|
||||||
context,
|
|
||||||
orderResult.order[input.outputKey],
|
|
||||||
onBlurBackground,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
38
packages/flutter_product_page/lib/src/models/product.dart
Normal file
38
packages/flutter_product_page/lib/src/models/product.dart
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/// The product page shop class contains all the required information
|
||||||
|
class Product {
|
||||||
|
/// Constructor for the product.
|
||||||
|
Product({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.imageUrl,
|
||||||
|
required this.category,
|
||||||
|
required this.price,
|
||||||
|
required this.hasDiscount,
|
||||||
|
this.discountPrice,
|
||||||
|
this.quantity = 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The unique identifier for the product.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// The name of the product.
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// The image URL of the product.
|
||||||
|
final String imageUrl;
|
||||||
|
|
||||||
|
/// The category of the product.
|
||||||
|
final String category;
|
||||||
|
|
||||||
|
/// The price of the product.
|
||||||
|
final double price;
|
||||||
|
|
||||||
|
/// Whether the product has a discount or not.
|
||||||
|
final bool hasDiscount;
|
||||||
|
|
||||||
|
/// The discounted price of the product. Only used if [hasDiscount] is true.
|
||||||
|
final double? discountPrice;
|
||||||
|
|
||||||
|
/// Quantity for the product.
|
||||||
|
final int quantity;
|
||||||
|
}
|
|
@ -34,7 +34,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
|
||||||
|
@ -116,7 +116,7 @@ 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(),
|
||||||
|
|
|
@ -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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -35,10 +35,10 @@ 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.
|
||||||
|
|
|
@ -16,51 +16,169 @@ 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(
|
|
||||||
onPressed: () => configuration.onCompleteUserStory(context),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 32.0,
|
|
||||||
vertical: 8.0,
|
|
||||||
),
|
|
||||||
child: Text("Finish Order".toUpperCase()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
var content = Column(
|
|
||||||
children: [
|
|
||||||
const Spacer(),
|
|
||||||
Text("#123456", style: theme.textTheme.titleLarge),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
"Order Succesfully Placed!",
|
|
||||||
style: theme.textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"Thank you for your order!",
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
"Your order will be delivered soon.",
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
"Do you want to order again?",
|
|
||||||
style: theme.textTheme.bodyMedium,
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
finishOrderButton,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
"Confirmation",
|
||||||
|
style: theme.textTheme.headlineLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: Center(
|
child: Column(
|
||||||
child: content,
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 32,
|
||||||
|
top: 32,
|
||||||
|
right: 32,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Success!",
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Thank you Peter for your order!",
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"The order was placed at Bakkerij de Goudkorst."
|
||||||
|
" You can pick this"
|
||||||
|
" up on Monday, February 7 at 1:00 PM.",
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"If you want, you can place another order in this street.",
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 32,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"Weekly offers",
|
||||||
|
style: theme.textTheme.headlineSmall
|
||||||
|
?.copyWith(color: Colors.black),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 4,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 272,
|
||||||
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 32),
|
||||||
|
_discount(context),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_discount(context),
|
||||||
|
const SizedBox(width: 32),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
FilledButton(
|
||||||
|
onPressed: () {
|
||||||
|
configuration.onCompleteUserStory.call(context);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"Place another order",
|
||||||
|
style: theme.textTheme.displayLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _discount(BuildContext context) {
|
||||||
|
var theme = Theme.of(context);
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
width: MediaQuery.of(context).size.width - 64,
|
||||||
|
height: 200,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(10),
|
||||||
|
topRight: Radius.circular(10),
|
||||||
|
bottomLeft: Radius.circular(10),
|
||||||
|
bottomRight: Radius.circular(10),
|
||||||
|
),
|
||||||
|
child: Image.network(
|
||||||
|
"https://picsum.photos/150",
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
height: 38,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.black,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(10),
|
||||||
|
topRight: Radius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
|
child: Text(
|
||||||
|
"Butcher Puurvlees",
|
||||||
|
style: theme.textTheme.headlineSmall?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
child: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(10),
|
||||||
|
bottomRight: Radius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
height: 68,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
|
child: Text(
|
||||||
|
"Chicken legs, now for 4,99",
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -136,7 +136,10 @@ Widget _defaultProductItemBuilder(
|
||||||
await showModalBottomSheet(
|
await showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
backgroundColor: theme.colorScheme.surface,
|
backgroundColor: theme.colorScheme.surface,
|
||||||
builder: (context) => ProductItemPopup(product: product, configuration: configuration)
|
builder: (context) => ProductItemPopup(
|
||||||
|
product: product,
|
||||||
|
configuration: configuration,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
|
|
Loading…
Reference in a new issue