feat: userstory (#13)

* feat: add interface to shopping cart

* feat: add interface to flutter_order_details

* feat: add interface to flutter_product_page

* feat: add userstory

* feat: cleanup configuration files

* feat: add builders for productpage

* feat: add filter screen and widgets

* feat: remove listenablebuilders from cart

* feat: remove listenablebuilders from order details

* feat: remove listenablebuilder from product page

---------

Co-authored-by: mike doornenbal <mikedoornenbal9@icloud.com>
This commit is contained in:
mike doornenbal 2024-07-12 09:49:58 +02:00 committed by GitHub
parent fd8afbde03
commit 88cefb047b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
94 changed files with 2488 additions and 3340 deletions

View file

@ -1,8 +1,9 @@
/// Flutter component for shopping cart.
library flutter_order_details;
export "package:flutter_form_wizard/flutter_form.dart";
export "src/configuration/order_detail_configuration.dart";
export "src/configuration/order_detail_localization.dart";
export "src/configuration/order_detail_title_style.dart";
export "src/models/order_result.dart";
export "src/widgets/order_detail_screen.dart";
export "src/configuration/order_detail_translations.dart";
export "src/order_detail_screen.dart";
export "src/widgets/order_succes.dart";

View file

@ -1,537 +1,71 @@
// ignore_for_file: avoid_annotating_with_dynamic
import "package:animated_toggle/animated_toggle.dart";
import "package:flutter/material.dart";
import "package:flutter_form_wizard/flutter_form.dart";
import "package:flutter_order_details/flutter_order_details.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Configuration for the order detail screen.
class OrderDetailConfiguration {
/// Constructor for the order detail configuration.
OrderDetailConfiguration({
required this.onCompleted,
this.pages = _defaultPages,
this.localization = const OrderDetailLocalization(),
this.appBar = _defaultAppBar,
this.nextbuttonBuilder = _defaultNextButtonBuilder,
const OrderDetailConfiguration({
required this.shoppingService,
required this.onNextStep,
required this.onStepsCompleted,
required this.onCompleteOrderDetails,
this.pages,
this.translations = const OrderDetailTranslations(),
this.appBarBuilder,
this.nextbuttonBuilder,
this.orderSuccessBuilder,
});
/// The shopping service that is used
final ShoppingService shoppingService;
/// The different steps that the user has to go through to complete the order.
/// Each step contains a list of fields that the user has to fill in.
final List<FlutterFormPage> Function(BuildContext context) pages;
final List<FlutterFormPage> Function(BuildContext context)? pages;
/// Callback function that is called when the user has completed the order.
/// The result of the order is passed as an argument to the function.
final Function(dynamic value) onCompleted;
final Function(
String shopId,
List<Product> products,
Map<int, Map<String, dynamic>> value,
OrderDetailConfiguration configuration,
) onStepsCompleted;
/// Callback function that is called when the user has completed a step.
final Function(
int currentStep,
Map<String, dynamic> data,
FlutterFormController controller,
) onNextStep;
/// Localization for the order detail screen.
final OrderDetailLocalization localization;
final OrderDetailTranslations translations;
/// Optional app bar that you can pass to the order detail screen.
final AppBar Function(
BuildContext context,
OrderDetailLocalization localizations,
) appBar;
final PreferredSizeWidget? Function(BuildContext context, String title)?
appBarBuilder;
/// Optional next button builder that you can pass to the order detail screen.
final Widget Function(
int a,
int currentStep,
// ignore: avoid_positional_boolean_parameters
bool b,
bool checkingPages,
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",
),
],
),
),
),
];
)? nextbuttonBuilder;
/// Optional builder for the order success screen.
final Widget Function(
BuildContext context,
OrderDetailConfiguration,
Map<int, Map<String, dynamic>> orderDetails,
)? orderSuccessBuilder;
/// This function is called after the order has been completed and
/// the success screen has been shown.
final Function(BuildContext context, OrderDetailConfiguration configuration)
onCompleteOrderDetails;
}

View file

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

View file

@ -1,7 +1,7 @@
/// Localizations for the order detail page.
class OrderDetailLocalization {
class OrderDetailTranslations {
/// Constructor for the order detail localization.
const OrderDetailLocalization({
const OrderDetailTranslations({
this.nextButton = "Order",
this.completeButton = "Complete",
this.orderDetailsTitle = "Information",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,46 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_form_wizard/flutter_form.dart";
import "package:flutter_order_details/flutter_order_details.dart";
/// Order Detail Screen.
class OrderDetailScreen extends StatefulWidget {
/// Screen that builds all forms based on the configuration.
const OrderDetailScreen({
required this.configuration,
super.key,
});
/// Configuration for the screen.
final OrderDetailConfiguration configuration;
@override
State<OrderDetailScreen> createState() => _OrderDetailScreenState();
}
class _OrderDetailScreenState extends State<OrderDetailScreen> {
@override
Widget build(BuildContext context) {
var controller = FlutterFormController();
return Scaffold(
appBar: widget.configuration.appBar
.call(context, widget.configuration.localization),
body: FlutterForm(
formController: controller,
options: FlutterFormOptions(
nextButton: (a, b) => widget.configuration.nextbuttonBuilder(
a,
b,
context,
widget.configuration,
controller,
),
pages: widget.configuration.pages.call(context),
onFinished: (data) {
widget.configuration.onCompleted.call(data);
},
onNext: (step, data) {},
),
),
);
}
}

View file

@ -1,21 +1,31 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_order_details/flutter_order_details.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Default order success widget.
class DefaultOrderSucces extends StatelessWidget {
/// Constructor for the DefaultOrderSucces.
const DefaultOrderSucces({
required this.configuration,
required this.orderDetails,
super.key,
});
/// Configuration for the user-story.
final FlutterShoppingConfiguration configuration;
/// Configuration for the user-stor
final OrderDetailConfiguration configuration;
/// Order details.
final Map<int, Map<String, dynamic>> orderDetails;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var discountedProducts = configuration
.shoppingService.productService.products
.where((product) => product.hasDiscount)
.toList();
return Scaffold(
appBar: AppBar(
title: Text(
@ -42,16 +52,19 @@ class DefaultOrderSucces extends StatelessWidget {
height: 4,
),
Text(
"Thank you Peter for your order!",
"Thank you ${orderDetails[0]!['name']} for your order!",
style: theme.textTheme.bodyMedium,
),
const SizedBox(
height: 16,
),
Text(
"The order was placed at Bakkerij de Goudkorst."
"The order was placed"
// ignore: lines_longer_than_80_chars
" at ${configuration.shoppingService.shopService.selectedShop?.name}."
" You can pick this"
" up on Monday, February 7 at 1:00 PM.",
" up ${orderDetails[1]!['date']} at"
" ${orderDetails[1]!['multipleChoice']}.",
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.center,
),
@ -84,9 +97,19 @@ class DefaultOrderSucces extends StatelessWidget {
scrollDirection: Axis.horizontal,
children: [
const SizedBox(width: 32),
_discount(context),
const SizedBox(width: 8),
_discount(context),
// _discount(context),
// const SizedBox(width: 8),
// _discount(context),
for (var product in discountedProducts) ...[
_discount(
context,
product,
configuration.shoppingService.shopService.selectedShop!,
),
const SizedBox(
width: 8,
),
],
const SizedBox(width: 32),
],
),
@ -98,7 +121,8 @@ class DefaultOrderSucces extends StatelessWidget {
width: double.infinity,
child: FilledButton(
onPressed: () async {
configuration.onCompleteUserStory.call(context);
configuration.onCompleteOrderDetails
.call(context, configuration);
},
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
@ -125,7 +149,7 @@ class DefaultOrderSucces extends StatelessWidget {
}
}
Widget _discount(BuildContext context) {
Widget _discount(BuildContext context, Product product, Shop shop) {
var theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
@ -139,14 +163,11 @@ Widget _discount(BuildContext context) {
child: Stack(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10),
borderRadius: BorderRadius.circular(
10,
),
child: Image.network(
"https://picsum.photos/150",
product.imageUrl,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
@ -166,7 +187,7 @@ Widget _discount(BuildContext context) {
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
"Butcher Puurvlees",
shop.name,
style: theme.textTheme.headlineSmall?.copyWith(
color: Colors.white,
),
@ -189,7 +210,7 @@ Widget _discount(BuildContext context) {
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
"Chicken legs, now for 4,99",
"${product.name}, now for ${product.price.toStringAsFixed(2)}",
style: theme.textTheme.bodyMedium,
),
),

View file

@ -1,10 +1,10 @@
name: flutter_order_details
description: "A Flutter module for order details."
version: 2.0.0
publish_to: 'none'
publish_to: "none"
environment:
sdk: '>=3.3.0 <4.0.0'
sdk: ">=3.3.0 <4.0.0"
dependencies:
flutter:
@ -17,6 +17,12 @@ dependencies:
git:
url: https://github.com/Iconica-Development/flutter_form_wizard
ref: 6.5.0
flutter_shopping_interface:
git:
url: https://github.com/Iconica-Development/flutter_shopping
path: packages/flutter_shopping_interface
ref: 2.0.0
collection: ^1.18.0
dev_dependencies:
flutter_test:
@ -27,4 +33,3 @@ dev_dependencies:
ref: 7.0.0
flutter:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,28 +1,14 @@
import "package:flutter/material.dart";
import "package:flutter_nested_categories/flutter_nested_categories.dart";
import "package:flutter_product_page/src/services/shopping_cart_notifier.dart";
import "package:flutter_product_page/src/ui/components/product_item.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// A function that is called when a product is added to the cart.
Product onAddToCartWrapper(
ProductPageConfiguration configuration,
ShoppingCartNotifier shoppingCartNotifier,
Product product,
) {
shoppingCartNotifier.productsChanged();
configuration.onAddToCart(product);
return product;
}
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_product_page/src/widgets/defaults/default_product_item.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Generates a [CategoryList] from a list of [Product]s and a
/// [ProductPageConfiguration].
Widget getCategoryList(
BuildContext context,
ProductPageConfiguration configuration,
ShoppingCartNotifier shoppingCartNotifier,
List<Product> products,
) {
var theme = Theme.of(context);
@ -39,18 +25,15 @@ Widget getCategoryList(
categorizedProducts.forEach((categoryName, productList) {
var productWidgets = productList
.map(
(product) => configuration.productBuilder != null
? configuration.productBuilder!(context, product)
: ProductItem(
product: product,
onProductDetail: configuration.onProductDetail,
onAddToCart: (Product product) => onAddToCartWrapper(
configuration,
shoppingCartNotifier,
product,
),
localizations: configuration.localizations,
),
(product) =>
configuration.productBuilder
?.call(context, product, configuration) ??
DefaultProductItem(
product: product,
onAddToCart: configuration.onAddToCart,
onProductDetail: configuration.onProductDetail!,
translations: configuration.translations,
),
)
.toList();
var category = Category(

View file

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

View file

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

View file

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

View file

@ -1,252 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_product_page/src/services/category_service.dart";
import "package:flutter_product_page/src/services/selected_shop_service.dart";
import "package:flutter_product_page/src/services/shopping_cart_notifier.dart";
import "package:flutter_product_page/src/ui/components/shop_selector.dart";
import "package:flutter_product_page/src/ui/components/weekly_discount.dart";
/// A page that displays products.
class ProductPage extends StatelessWidget {
/// Constructor for the product page.
ProductPage({
required this.configuration,
this.initialBuildShopId,
super.key,
});
/// Configuration for the product page.
final ProductPageConfiguration configuration;
/// An optional initial shop ID to select. This overrides the initialShopId
/// from the configuration.
final String? initialBuildShopId;
late final SelectedShopService _selectedShopService = SelectedShopService();
late final ShoppingCartNotifier _shoppingCartNotifier =
ShoppingCartNotifier();
@override
Widget build(BuildContext context) => Padding(
padding: configuration.pagePadding,
child: FutureBuilder(
future: configuration.shops,
builder: (BuildContext context, AsyncSnapshot data) {
if (data.connectionState == ConnectionState.waiting) {
return const Align(
alignment: Alignment.center,
child: CircularProgressIndicator.adaptive(),
);
}
if (data.hasError) {
return configuration.errorBuilder!(
context,
data.error,
data.stackTrace,
);
}
List<ProductPageShop>? shops = data.data;
if (shops == null || shops.isEmpty) {
return configuration.errorBuilder!(context, null, null);
}
if (initialBuildShopId != null) {
ProductPageShop? initialShop;
for (var shop in shops) {
if (shop.id == initialBuildShopId) {
initialShop = shop;
break;
}
}
_selectedShopService.selectShop(initialShop ?? shops.first);
} else if (configuration.initialShopId != null) {
ProductPageShop? initialShop;
for (var shop in shops) {
if (shop.id == configuration.initialShopId) {
initialShop = shop;
break;
}
}
_selectedShopService.selectShop(initialShop ?? shops.first);
} else {
_selectedShopService.selectShop(shops.first);
}
return ListenableBuilder(
listenable: _selectedShopService,
builder: (BuildContext context, Widget? _) {
configuration.onShopSelectionChange?.call(
_selectedShopService.selectedShop!,
);
return _ProductPage(
configuration: configuration,
selectedShopService: _selectedShopService,
shoppingCartNotifier: _shoppingCartNotifier,
shops: shops,
);
},
);
},
),
);
}
class _ProductPage extends StatelessWidget {
const _ProductPage({
required this.configuration,
required this.selectedShopService,
required this.shoppingCartNotifier,
required this.shops,
});
final ProductPageConfiguration configuration;
final SelectedShopService selectedShopService;
final ShoppingCartNotifier shoppingCartNotifier;
final List<ProductPageShop> shops;
void _onTapChangeShop(ProductPageShop shop) {
selectedShopService.selectShop(shop);
}
@override
Widget build(BuildContext context) {
var pageContent = SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ShopSelector(
configuration: configuration,
selectedShopService: selectedShopService,
shops: shops,
onTap: _onTapChangeShop,
),
_ShopContents(
configuration: configuration,
selectedShopService: selectedShopService,
shoppingCartNotifier: shoppingCartNotifier,
),
],
),
);
return Stack(
children: [
pageContent,
Align(
alignment: Alignment.bottomCenter,
child: configuration.navigateToShoppingCartBuilder(
context,
configuration,
shoppingCartNotifier,
),
),
],
);
}
}
class _ShopContents extends StatelessWidget {
const _ShopContents({
required this.configuration,
required this.selectedShopService,
required this.shoppingCartNotifier,
});
final ProductPageConfiguration configuration;
final SelectedShopService selectedShopService;
final ShoppingCartNotifier shoppingCartNotifier;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Padding(
padding: EdgeInsets.symmetric(
horizontal: configuration.pagePadding.horizontal,
),
child: FutureBuilder(
// ignore: discarded_futures
future: configuration.getProducts(
selectedShopService.selectedShop!,
),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Align(
alignment: Alignment.center,
child: CircularProgressIndicator.adaptive(),
);
}
if (snapshot.hasError) {
return configuration.errorBuilder!(
context,
snapshot.error,
snapshot.stackTrace,
);
}
var productPageContent = snapshot.data;
if (productPageContent == null ||
productPageContent.products.isEmpty) {
return configuration.noContentBuilder!(context);
}
var productList = Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
child: Column(
children: [
// Products
getCategoryList(
context,
configuration,
shoppingCartNotifier,
productPageContent.products,
),
// Bottom padding so the last product is not cut off
// by the to shopping cart button.
const SizedBox(height: 48),
],
),
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Discounted product
if (productPageContent.discountedProduct != null) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: WeeklyDiscount(
configuration: configuration,
product: productPageContent.discountedProduct!,
),
),
],
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
child: Text(
"What would you like to order?",
style: theme.textTheme.titleLarge,
textAlign: TextAlign.start,
),
),
productList,
],
);
},
),
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,16 +1,17 @@
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
import "package:skeletonizer/skeletonizer.dart";
/// Product item widget.
class ProductItem extends StatelessWidget {
class DefaultProductItem extends StatelessWidget {
/// Constructor for the product item widget.
const ProductItem({
const DefaultProductItem({
required this.product,
required this.onProductDetail,
required this.onAddToCart,
required this.localizations,
required this.translations,
super.key,
});
@ -18,13 +19,17 @@ class ProductItem extends StatelessWidget {
final Product product;
/// Function to call when the product detail is requested.
final Function(BuildContext context, Product selectedProduct) onProductDetail;
final Function(
BuildContext context,
Product selectedProduct,
String closeText,
) onProductDetail;
/// Function to call when the product is added to the cart.
final Function(Product selectedProduct) onAddToCart;
/// Localizations for the product page.
final ProductPageLocalization localizations;
final ProductPageTranslations translations;
/// Size of the product image.
static const double imageSize = 44;
@ -46,7 +51,7 @@ class ProductItem extends StatelessWidget {
fit: BoxFit.cover,
placeholder: (context, url) => loadingImageSkeleton,
errorWidget: (context, url, error) => Tooltip(
message: localizations.failedToLoadImageExplenation,
message: translations.failedToLoadImageExplenation,
child: Container(
width: 48,
height: 48,
@ -74,7 +79,11 @@ class ProductItem extends StatelessWidget {
var productInformationIcon = Padding(
padding: const EdgeInsets.only(left: 4),
child: IconButton(
onPressed: () => onProductDetail(context, product),
onPressed: () => onProductDetail(
context,
product,
translations.close,
),
icon: Icon(
Icons.info_outline,
color: theme.colorScheme.primary,

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Horizontal list of items.
class HorizontalListItems extends StatelessWidget {
@ -14,7 +14,7 @@ class HorizontalListItems extends StatelessWidget {
});
/// List of items.
final List<ProductPageShop> shops;
final List<Shop> shops;
/// Selected item.
final String selectedItem;
@ -26,7 +26,7 @@ class HorizontalListItems extends StatelessWidget {
final double paddingOnButtons;
/// Callback when an item is tapped.
final Function(ProductPageShop shop) onTap;
final Function(Shop shop) onTap;
@override
Widget build(BuildContext context) {

View file

@ -1,12 +1,12 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// A popup that displays the product item.
class ProductItemPopup extends StatelessWidget {
/// Constructor for the product item popup.
const ProductItemPopup({
required this.product,
required this.configuration,
required this.closeText,
super.key,
});
@ -14,7 +14,7 @@ class ProductItemPopup extends StatelessWidget {
final Product product;
/// Configuration for the product page.
final ProductPageConfiguration configuration;
final String closeText;
@override
Widget build(BuildContext context) {
@ -49,7 +49,7 @@ class ProductItemPopup extends StatelessWidget {
vertical: 8.0,
),
child: Text(
configuration.localizations.close,
closeText,
style: theme.textTheme.displayLarge,
),
),

View file

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

View file

@ -1,5 +1,5 @@
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// SpacedWrap is a widget that wraps a list of items that are spaced out and
/// fill the available width.
@ -16,7 +16,7 @@ class SpacedWrap extends StatelessWidget {
});
/// List of items.
final List<ProductPageShop> shops;
final List<Shop> shops;
/// Selected item.
final String selectedItem;
@ -31,7 +31,7 @@ class SpacedWrap extends StatelessWidget {
final double paddingOnButtons;
/// Callback when an item is tapped.
final Function(ProductPageShop shop) onTap;
final Function(Shop shop) onTap;
@override
Widget build(BuildContext context) {

View file

@ -1,6 +1,7 @@
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// A widget that displays a weekly discount.
class WeeklyDiscount extends StatelessWidget {
@ -27,7 +28,7 @@ class WeeklyDiscount extends StatelessWidget {
var bottomText = Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
configuration.getDiscountDescription!(product),
configuration.discountDescription!(product),
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.left,
),
@ -50,7 +51,7 @@ class WeeklyDiscount extends StatelessWidget {
Icons.error_outline_rounded,
color: Colors.red,
),
Text(configuration.localizations.failedToLoadImageExplenation),
Text(configuration.translations.failedToLoadImageExplenation),
],
),
),
@ -86,7 +87,7 @@ class WeeklyDiscount extends StatelessWidget {
horizontal: 16,
),
child: Text(
configuration.localizations.discountTitle,
configuration.translations.discountTitle,
style: theme.textTheme.headlineSmall,
textAlign: TextAlign.left,
),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +0,0 @@
import "package:flutter_shopping/flutter_shopping.dart";
/// Example implementation of storing an order in a database.
void storeOrderInDatabase(List<Product> products, OrderResult result) {
return;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,52 +0,0 @@
import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_shopping/src/widgets/default_order_failed_widget.dart";
import "package:flutter_shopping/src/widgets/default_order_succes_widget.dart";
import "package:go_router/go_router.dart";
/// All the routes for the shopping story.
List<GoRoute> getShoppingStoryRoutes({
required FlutterShoppingConfiguration configuration,
}) =>
<GoRoute>[
GoRoute(
name: FlutterShoppingNameRoutes.shop,
path: FlutterShoppingPathRoutes.shop,
builder: (context, state) => configuration.shopBuilder(
context,
state.uri.queryParameters["id"],
state.uri.queryParameters["street"],
),
),
GoRoute(
name: FlutterShoppingNameRoutes.shoppingCart,
path: FlutterShoppingPathRoutes.shoppingCart,
builder: (context, state) => configuration.shoppingCartBuilder(context),
),
GoRoute(
name: FlutterShoppingNameRoutes.orderDetails,
path: FlutterShoppingPathRoutes.orderDetails,
builder: (context, state) => configuration.orderDetailsBuilder != null
? configuration.orderDetailsBuilder!(context)
: OrderDetailScreen(
configuration: OrderDetailConfiguration(
onCompleted: (result) {
context.go(FlutterShoppingPathRoutes.orderSuccess);
},
),
),
),
GoRoute(
name: FlutterShoppingNameRoutes.orderSuccess,
path: FlutterShoppingPathRoutes.orderSuccess,
builder: (context, state) => configuration.orderSuccessBuilder != null
? configuration.orderSuccessBuilder!(context)
: DefaultOrderSucces(configuration: configuration),
),
GoRoute(
name: FlutterShoppingNameRoutes.orderFailed,
path: FlutterShoppingPathRoutes.orderFailed,
builder: (context, state) => configuration.orderFailedBuilder != null
? configuration.orderFailedBuilder!(context)
: DefaultOrderFailed(configuration: configuration),
),
];

View file

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

View file

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

View file

@ -1,10 +1,10 @@
name: flutter_shopping
description: "A new Flutter project."
publish_to: 'none'
publish_to: "none"
version: 2.0.0
environment:
sdk: '>=3.3.4 <4.0.0'
sdk: ">=3.3.4 <4.0.0"
dependencies:
flutter:
@ -25,6 +25,16 @@ dependencies:
url: https://github.com/Iconica-Development/flutter_shopping
ref: 2.0.0
path: packages/flutter_order_details
flutter_shopping_interface:
git:
url: https://github.com/Iconica-Development/flutter_shopping
ref: 2.0.0
path: packages/flutter_shopping_interface
flutter_shopping_local:
git:
url: https://github.com/Iconica-Development/flutter_shopping
ref: 2.0.0
path: packages/flutter_shopping_local
dev_dependencies:
flutter_test:
@ -35,4 +45,3 @@ dev_dependencies:
ref: 7.0.0
flutter:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,51 @@
import "package:flutter/material.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Default confirm order button.
class DefaultConfirmOrderButton extends StatelessWidget {
/// Constructor for the default confirm order button.
const DefaultConfirmOrderButton({
required this.configuration,
required this.onConfirmOrder,
super.key,
});
/// Configuration for the shopping cart.
final ShoppingCartConfig configuration;
/// Function to call when the order is confirmed.
final Function(List<Product> products) onConfirmOrder;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: configuration.service.products.isEmpty
? null
: () => onConfirmOrder(
configuration.service.products,
),
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.translations.placeOrder,
style: theme.textTheme.displayLarge,
),
),
),
),
);
}
}

View file

@ -0,0 +1,134 @@
import "package:flutter/material.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
import "package:flutter_shopping_cart/src/widgets/product_item_popup.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Default shopping cart item.
class DefaultShoppingCartItem extends StatelessWidget {
/// Constructor for the default shopping cart item.
const DefaultShoppingCartItem({
required this.product,
required this.configuration,
required this.onItemAddedRemoved,
super.key,
});
/// Product to display.
final Product product;
/// Shopping cart configuration.
final ShoppingCartConfig configuration;
/// Function that is called when an item is added or removed.
final Function() onItemAddedRemoved;
@override
Widget build(BuildContext context) {
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(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
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: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (product.hasDiscount && product.discountPrice != null) ...[
Text(
product.discountPrice!.toStringAsFixed(2),
style: theme.textTheme.labelSmall,
),
] else ...[
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: () {
configuration.service.removeOneProduct(product);
onItemAddedRemoved();
},
),
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: () {
configuration.service.addProduct(product);
onItemAddedRemoved();
},
),
],
),
],
),
),
);
}
}

View file

@ -0,0 +1,42 @@
import "package:flutter/material.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
/// Default sum bottom sheet builder.
class DefaultSumBottomSheetBuilder extends StatelessWidget {
/// Constructor for the default sum bottom sheet builder.
const DefaultSumBottomSheetBuilder({
required this.configuration,
super.key,
});
/// Configuration for the shopping cart.
final ShoppingCartConfig configuration;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var totalPrice = configuration.service.products.fold<double>(
0,
(previousValue, element) =>
previousValue +
(element.discountPrice ?? element.price) * element.quantity,
);
return Padding(
padding: configuration.bottomPadding,
child: Row(
children: [
Text(
configuration.translations.sum,
style: theme.textTheme.titleMedium,
),
const Spacer(),
Text(
"${totalPrice.toStringAsFixed(2)}",
style: theme.textTheme.bodyMedium,
),
],
),
);
}
}

View file

@ -1,5 +1,6 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// A popup that displays the product item.
class ProductItemPopup extends StatelessWidget {
@ -49,7 +50,7 @@ class ProductItemPopup extends StatelessWidget {
vertical: 8.0,
),
child: Text(
configuration.localizations.close,
configuration.translations.close,
style: theme.textTheme.displayLarge,
),
),

View file

@ -1,8 +1,12 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
import "package:flutter_shopping_cart/src/widgets/default_appbar.dart";
import "package:flutter_shopping_cart/src/widgets/default_confirm_order_button.dart";
import "package:flutter_shopping_cart/src/widgets/default_shopping_cart_item.dart";
import "package:flutter_shopping_cart/src/widgets/default_sum_bottom_sheet_builder.dart";
/// Shopping cart screen widget.
class ShoppingCartScreen<T extends Product> extends StatelessWidget {
class ShoppingCartScreen extends StatefulWidget {
/// Creates a shopping cart screen.
const ShoppingCartScreen({
required this.configuration,
@ -10,86 +14,85 @@ class ShoppingCartScreen<T extends Product> extends StatelessWidget {
});
/// Configuration for the shopping cart screen.
final ShoppingCartConfig<T> configuration;
final ShoppingCartConfig configuration;
@override
State<ShoppingCartScreen> createState() => _ShoppingCartScreenState();
}
class _ShoppingCartScreenState extends State<ShoppingCartScreen> {
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var productBuilder = SingleChildScrollView(
child: Column(
children: [
if (configuration.titleBuilder != null) ...{
configuration.titleBuilder!(context),
} else ...{
Padding(
padding: const EdgeInsets.symmetric(
vertical: 32,
),
child: Row(
children: [
Text(
configuration.localizations.cartTitle,
style: theme.textTheme.titleLarge,
textAlign: TextAlign.start,
),
],
),
),
},
ListenableBuilder(
listenable: configuration.productService,
builder: (context, _) {
var products = configuration.productService.products;
if (products.isEmpty) {
return configuration.noContentBuilder(context);
}
return Column(
children: [
for (var product in products)
configuration.productItemBuilder(
context,
configuration.localizations.locale,
product,
configuration.productService,
configuration,
),
// Additional whitespace at the bottom to make sure the
// last product(s) are not hidden by the bottom sheet.
SizedBox(
height: configuration.confirmOrderButtonHeight +
configuration.sumBottomSheetHeight,
),
],
);
},
),
],
),
);
return Scaffold(
appBar: configuration.appBar ??
AppBar(
title: Text(
"Shopping cart",
style: theme.textTheme.headlineLarge,
),
),
appBar: widget.configuration.appBarBuilder?.call(context) ??
const DefaultAppbar(),
body: SafeArea(
child: Stack(
fit: StackFit.expand,
children: [
Padding(
padding: configuration.padding,
child: productBuilder,
padding: widget.configuration.pagePadding,
child: SingleChildScrollView(
child: Column(
children: [
if (widget.configuration.titleBuilder != null) ...{
widget.configuration.titleBuilder!(
context,
widget.configuration.translations.cartTitle,
),
} else ...{
Padding(
padding: const EdgeInsets.symmetric(
vertical: 32,
),
child: Row(
children: [
Text(
widget.configuration.translations.cartTitle,
style: theme.textTheme.titleLarge,
textAlign: TextAlign.start,
),
],
),
),
},
Column(
children: [
for (var product
in widget.configuration.service.products)
widget.configuration.productItemBuilder?.call(
context,
product,
widget.configuration,
) ??
DefaultShoppingCartItem(
product: product,
configuration: widget.configuration,
onItemAddedRemoved: () {
setState(() {});
},
),
// Additional whitespace at
// the bottom to make sure the last
// product(s) are not hidden by the bottom sheet.
SizedBox(
height:
widget.configuration.confirmOrderButtonHeight +
widget.configuration.sumBottomSheetHeight,
),
],
),
],
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: _BottomSheet<T>(
configuration: configuration,
child: _BottomSheet(
configuration: widget.configuration,
),
),
],
@ -99,124 +102,30 @@ class ShoppingCartScreen<T extends Product> extends StatelessWidget {
}
}
class _BottomSheet<T extends Product> extends StatelessWidget {
class _BottomSheet extends StatelessWidget {
const _BottomSheet({
required this.configuration,
super.key,
});
final ShoppingCartConfig<T> configuration;
@override
Widget build(BuildContext context) {
var placeOrderButton = ListenableBuilder(
listenable: configuration.productService,
builder: (BuildContext context, Widget? child) =>
configuration.confirmOrderButtonBuilder != null
? configuration.confirmOrderButtonBuilder!(context)
: _DefaultConfirmOrderButton<T>(configuration: configuration),
);
var bottomSheet = ListenableBuilder(
listenable: configuration.productService,
builder: (BuildContext context, Widget? child) =>
configuration.sumBottomSheetBuilder != null
? configuration.sumBottomSheetBuilder!(context)
: _DefaultSumBottomSheet(configuration: configuration),
);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
bottomSheet,
placeOrderButton,
],
);
}
}
class _DefaultConfirmOrderButton<T extends Product> extends StatelessWidget {
const _DefaultConfirmOrderButton({
required this.configuration,
});
final ShoppingCartConfig<T> configuration;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
void onConfirmOrderPressed(List<T> products) {
if (configuration.onConfirmOrder == null) {
return;
}
if (products.isEmpty) {
return;
}
configuration.onConfirmOrder!(products);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () => onConfirmOrderPressed(
configuration.productService.products,
),
style: theme.filledButtonTheme.style?.copyWith(
backgroundColor: WidgetStateProperty.all(
theme.colorScheme.primary,
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12,
),
child: Text(
configuration.localizations.placeOrder,
style: theme.textTheme.displayLarge,
),
),
),
),
);
}
}
class _DefaultSumBottomSheet extends StatelessWidget {
const _DefaultSumBottomSheet({
required this.configuration,
});
final ShoppingCartConfig configuration;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var totalPrice = configuration.productService.products
.map((product) => product.price * product.quantity)
.fold(0.0, (a, b) => a + b);
return Padding(
padding: configuration.bottomPadding,
child: Row(
Widget build(BuildContext context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
configuration.localizations.sum,
style: theme.textTheme.titleMedium,
),
const Spacer(),
Text(
"${totalPrice.toStringAsFixed(2)}",
style: theme.textTheme.bodyMedium,
),
configuration.sumBottomSheetBuilder?.call(context, configuration) ??
DefaultSumBottomSheetBuilder(
configuration: configuration,
),
configuration.confirmOrderButtonBuilder?.call(
context,
configuration,
configuration.onConfirmOrder,
) ??
DefaultConfirmOrderButton(
configuration: configuration,
onConfirmOrder: configuration.onConfirmOrder,
),
],
),
);
}
);
}

View file

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

View file

@ -5,8 +5,8 @@ import "package:flutter_shopping_interface/src/model/product.dart";
abstract class OrderService {
/// Create an order
Future<void> createOrder(
int shopId,
String shopId,
List<Product> products,
Map<String, dynamic> clientInformation,
Map<int, Map<String, dynamic>> clientInformation,
);
}

View file

@ -11,4 +11,13 @@ abstract class ProductService with ChangeNotifier {
/// Retrieve a list of categories
List<String> getCategories();
/// Get current Products
List<Product> get products;
/// Get current Products
List<String> get selectedCategories;
/// Select a category
void selectCategory(String category);
}

View file

@ -17,4 +17,7 @@ abstract class ShoppingCartService with ChangeNotifier {
/// Clears the shopping cart.
void clear();
/// The list of products in the shopping cart.
List<Product> get products;
}

View file

@ -5,11 +5,11 @@ import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
class LocalOrderService with ChangeNotifier implements OrderService {
@override
Future<void> createOrder(
int shopId,
String shopId,
List<Product> products,
Map<String, dynamic> clientInformation,
) {
// No use case for this method yet
throw UnimplementedError();
Map<int, Map<String, dynamic>> clientInformation,
) async {
// Create the order
notifyListeners();
}
}

View file

@ -4,10 +4,12 @@ import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Local product service
class LocalProductService with ChangeNotifier implements ProductService {
List<Product> _products = [];
List<Product> _allProducts = [];
final List<String> _selectedCategories = [];
@override
List<String> getCategories() =>
_products.map((e) => e.category).toSet().toList();
_allProducts.map((e) => e.category).toSet().toList();
@override
Future<Product> getProduct(String id) =>
@ -60,6 +62,42 @@ class LocalProductService with ChangeNotifier implements ProductService {
description: "This is a delicious Brown fish",
),
];
// only return items that match the selectedcategories
_allProducts = List.from(_products);
_products = _products.where((element) {
if (_selectedCategories.isEmpty) {
return true;
}
return _selectedCategories.contains(element.category);
}).toList();
return Future.value(_products);
}
@override
List<Product> get products => _products;
@override
void selectCategory(String category) {
if (_selectedCategories.contains(category)) {
_selectedCategories.remove(category);
} else {
_selectedCategories.add(category);
}
if (_selectedCategories.isEmpty) {
_products = List.from(_allProducts);
}
_products = _allProducts.where((element) {
if (_selectedCategories.isEmpty) {
return true;
}
return _selectedCategories.contains(element.category);
}).toList();
notifyListeners();
}
@override
List<String> get selectedCategories => _selectedCategories;
}

View file

@ -54,4 +54,7 @@ class LocalShoppingCartService
}
notifyListeners();
}
@override
List<Product> get products => _products;
}

View file

@ -1 +1,5 @@
export "local_order_service.dart";
export "local_product_service.dart";
export "local_shop_service.dart";
export "local_shopping_cart_service.dart";
export "local_shopping_service.dart";