feat: add new userstory structure

This commit is contained in:
mike doornenbal 2024-10-21 10:51:45 +02:00
parent 88cefb047b
commit efca65b6be
120 changed files with 3569 additions and 4560 deletions

View file

@ -1,3 +1,8 @@
## 3.0.0
- Refactored the project structure
- Added `flutter_shopping_interface` package
- Implemented default design
## 2.0.0
- Added `flutter_shopping_interface` package
- Implemented default design

View file

@ -9,7 +9,6 @@
.history
.svn/
migrate_working_dir/
.metadata
# IntelliJ related
*.iml
@ -20,30 +19,11 @@ migrate_working_dir/
# 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/
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
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
# env
*dotenv
build/

View file

@ -0,0 +1,6 @@
library firebase_shopping_repository;
export 'src/firebase_category_repository.dart';
export 'src/firebase_product_repository.dart';
export 'src/firebase_shop_repository.dart';
export 'src/firebase_shopping_cart_repository.dart';

View file

@ -0,0 +1,53 @@
import 'dart:async';
import 'package:rxdart/rxdart.dart';
import 'package:shopping_repository_interface/shopping_repository_interface.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class FirebaseCategoryRepository implements CategoryRepositoryInterface {
final StreamController<List<Category>> _categoryController =
BehaviorSubject<List<Category>>();
final StreamController<List<Category>> _selectedCategoriesController =
BehaviorSubject<List<Category>>();
List<Category> _selectedCategories = [];
List<Category> _categories = [];
@override
void deselectCategory(String? categoryId) {
_selectedCategories.removeWhere((category) => category.id == categoryId);
_selectedCategoriesController.add(_selectedCategories);
}
@override
Stream<List<Category>> getCategories() {
FirebaseFirestore.instance
.collection('shopping_category')
.snapshots()
.listen((event) {
List<Category> categories = [];
event.docs.forEach((element) {
categories.add(Category.fromMap(element.id, element.data()));
});
_categoryController.add(categories);
_categories = categories;
});
return _categoryController.stream;
}
@override
Stream<List<Category>?> getSelectedCategoryStream() {
_selectedCategoriesController.add(_selectedCategories);
return _selectedCategoriesController.stream;
}
@override
Category? selectCategory(String? categoryId) {
_selectedCategories
.add(_categories.firstWhere((category) => category.id == categoryId));
_selectedCategoriesController.add(_selectedCategories);
return _selectedCategories.last;
}
}

View file

@ -0,0 +1,60 @@
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:collection/collection.dart';
import 'package:rxdart/rxdart.dart';
import 'package:shopping_repository_interface/shopping_repository_interface.dart';
class FirebaseProductRepository implements ProductRepositoryInterface {
/// Shop one product
final List<Product> _products = [];
Product? _selectedProduct;
final StreamController<List<Product>> _productStream =
BehaviorSubject<List<Product>>();
@override
Product? getProduct(String? productId) =>
_products.firstWhere((product) => product.id == productId);
@override
Stream<List<Product>> getProducts(List<Category>? categories, String shopId) {
FirebaseFirestore.instance
.collection('shopping_products')
.doc(shopId)
.snapshots()
.listen((event) {
_products.clear();
if (event.data() == null) return;
var shopProducts = event.data()!['products'] as List<dynamic>;
print(categories);
if (categories != null && categories.isNotEmpty)
shopProducts = shopProducts
.where(
(product) => categories
.any((category) => category.name == product['category']),
)
.toList();
shopProducts.forEach((product) {
_products.add(Product.fromMap(product));
});
_productStream.add(_products);
});
return _productStream.stream;
}
@override
Product? selectProduct(String? productId) {
_selectedProduct =
_products.firstWhere((product) => product.id == productId);
return _selectedProduct;
}
@override
Product? getWeeklyOffer() =>
_products.firstWhereOrNull((product) => product.isDiscounted);
}

View file

@ -0,0 +1,47 @@
import 'dart:async';
import 'package:rxdart/rxdart.dart';
import 'package:shopping_repository_interface/shopping_repository_interface.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
class FirebaseShopRepository implements ShopRepositoryInterface {
final StreamController<List<Shop>> _shopController =
BehaviorSubject<List<Shop>>();
Shop? _selectedShop;
List<Shop> _shops = [];
@override
Shop? getSelectedShop() {
return _selectedShop;
}
@override
Shop? getShop(String? shopId) {
return _shops.firstWhere((element) => element.id == shopId);
}
@override
Stream<List<Shop>> getShops() {
FirebaseFirestore.instance
.collection('shopping_shop')
.snapshots()
.listen((event) {
List<Shop> shops = [];
event.docs.forEach((element) {
shops.add(Shop.fromMap(element.id, element.data()));
});
_shops = shops;
_shopController.add(shops);
});
return _shopController.stream;
}
@override
Shop? selectShop(String? shopId) {
_selectedShop = _shops.firstWhere((element) => element.id == shopId);
return _selectedShop;
}
}

View file

@ -0,0 +1,78 @@
import 'dart:async';
import 'package:rxdart/rxdart.dart';
import 'package:shopping_repository_interface/shopping_repository_interface.dart';
class FirebaseShoppingCartRepository
implements ShoppingCartRepositoryInterface {
var _cart = ShoppingCart(id: "1", products: []);
final StreamController<ShoppingCart> _shoppingCartController =
BehaviorSubject<ShoppingCart>();
@override
Future<void> addProductToCart(Product product) async {
var existingProducts = _cart.products;
var index = existingProducts.indexWhere((p) => p.id == product.id);
if (index != -1) {
existingProducts[index] = product.copyWith(
selectedAmount: existingProducts[index].selectedAmount + 1,
);
_cart = _cart.copyWith(
products: existingProducts,
totalAmount: _cart.totalAmount + product.price,
totalAmountWithDiscount: product.isDiscounted
? _cart.totalAmountWithDiscount + product.discountPrice
: _cart.totalAmountWithDiscount + product.price,
);
} else {
_cart = _cart.copyWith(
products: [...existingProducts, product.copyWith(selectedAmount: 1)],
totalAmount: _cart.totalAmount + product.price,
totalAmountWithDiscount: product.isDiscounted
? _cart.totalAmountWithDiscount + product.discountPrice
: _cart.totalAmountWithDiscount + product.price,
);
}
_shoppingCartController.add(_cart);
}
@override
Stream<int> getCartLength() {
return _shoppingCartController.stream.map((cart) => cart.products.length);
}
@override
Stream<ShoppingCart> getShoppingCart() {
return _shoppingCartController.stream;
}
@override
Future<void> removeProductFromCart(Product product) {
var existingProducts = _cart.products;
if (existingProducts.contains(product)) {
var index = existingProducts.indexOf(product);
if (product.selectedAmount == 1) {
existingProducts.removeAt(index);
} else {
existingProducts[index] = product.copyWith(
selectedAmount: existingProducts[index].selectedAmount - 1,
);
}
_cart = _cart.copyWith(
products: existingProducts,
totalAmount: _cart.totalAmount - product.price,
totalAmountWithDiscount: product.isDiscounted
? _cart.totalAmountWithDiscount - product.discountPrice
: _cart.totalAmountWithDiscount - product.price,
);
_shoppingCartController.add(_cart);
}
return Future.value();
}
}

View file

@ -0,0 +1,25 @@
name: firebase_shopping_repository
description: "A new Flutter package project."
version: 3.0.0
homepage: https://github.com/Iconica-Development/flutter_shopping/packages/firebase_shopping_repository
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
environment:
sdk: ^3.5.3
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
collection: ^1.18.0
rxdart: ^0.28.0
firebase_core: ^3.6.0
cloud_firestore: ^5.4.4
shopping_repository_interface:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
version: ^3.0.0
dev_dependencies:
flutter_test:
sdk: flutter

View file

@ -1,36 +0,0 @@
# flutter_order_details
This component contains TODO...
## Features
* TODO...
## Usage
First, TODO...
For a more detailed example you can see the [example](https://github.com/Iconica-Development/flutter_order_details/tree/main/example).
Or, you could run the example yourself:
```
git clone https://github.com/Iconica-Development/flutter_order_details.git
cd flutter_order_details
cd example
flutter run
```
## Issues
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_order_details) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
## Want to contribute
If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_order_details/pulls).
## Author
This flutter_order_details for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>

View file

@ -1,9 +0,0 @@
/// 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_translations.dart";
export "src/order_detail_screen.dart";
export "src/widgets/order_succes.dart";

View file

@ -1,71 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_order_details/flutter_order_details.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Configuration for the order detail screen.
class OrderDetailConfiguration {
/// Constructor for the order detail configuration.
const OrderDetailConfiguration({
required this.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;
/// 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(
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 OrderDetailTranslations translations;
/// Optional app bar that you can pass to the order detail screen.
final PreferredSizeWidget? Function(BuildContext context, String title)?
appBarBuilder;
/// Optional next button builder that you can pass to the order detail screen.
final Widget Function(
int currentStep,
// ignore: avoid_positional_boolean_parameters
bool checkingPages,
BuildContext context,
OrderDetailConfiguration configuration,
FlutterFormController controller,
)? nextbuttonBuilder;
/// Optional builder for the order success screen.
final Widget Function(
BuildContext context,
OrderDetailConfiguration,
Map<int, Map<String, dynamic>> orderDetails,
)? orderSuccessBuilder;
/// This function is called after the order has been completed and
/// the success screen has been shown.
final Function(BuildContext context, OrderDetailConfiguration configuration)
onCompleteOrderDetails;
}

View file

@ -1,18 +0,0 @@
/// Localizations for the order detail page.
class OrderDetailTranslations {
/// Constructor for the order detail localization.
const OrderDetailTranslations({
this.nextButton = "Order",
this.completeButton = "Complete",
this.orderDetailsTitle = "Information",
});
/// Next button localization.
final String nextButton;
/// Complete button localization.
final String completeButton;
/// Title for the order details page.
final String orderDetailsTitle;
}

View file

@ -1,68 +0,0 @@
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

@ -1,27 +0,0 @@
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

@ -1,69 +0,0 @@
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

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

View file

@ -1,35 +0,0 @@
name: flutter_order_details
description: "A Flutter module for order details."
version: 2.0.0
publish_to: "none"
environment:
sdk: ">=3.3.0 <4.0.0"
dependencies:
flutter:
sdk: flutter
animated_toggle:
git:
url: https://github.com/Iconica-Development/flutter_animated_toggle
ref: 0.0.3
flutter_form_wizard:
git:
url: https://github.com/Iconica-Development/flutter_form_wizard
ref: 6.5.0
flutter_shopping_interface:
git:
url: https://github.com/Iconica-Development/flutter_shopping
path: packages/flutter_shopping_interface
ref: 2.0.0
collection: ^1.18.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
flutter:

View file

@ -1,47 +0,0 @@
# flutter_product_page
This component allows you to easily create and manage the products for any shop. Easily highlight a specific product
and automatically see your products categorized. This package allows users to gather more information about a product,
add it to a custom implementable shopping cart and even navigate to your own shopping cart.
This component is very customizable, it allows you to adjust basically everything while providing clean defaults.
## Features
* Easily navigate between different shops,
* Show users a highlighted product,
* Integrate with your own shopping cart,
* Automatically categorized products, powered by the `flutter_nested_categories` package that you have full control over, even in this component.
## Usage
First, you must implement your own `Shop` and `Product` classes. Your shop class must extend from the `ProductPageShop` class provided by this module. Your `Product` class should extend from the `Product` class provided by this module.
Next, you can create a `ProductPage` or a `ProductPageScreen`. The choice for the former is when you do not want to create a new Scaffold and the latter for when you want to create a new Scaffold.
To show the page, you must configure what you want to show. Both the `ProductPage` and the `ProductPageScreen` take a parameter that is a `ProductPageConfiguration`. This allows you for a lot of customizability, including what shops there are and what products to show.
For a more detailed example you can see the [example](https://github.com/Iconica-Development/flutter_product_page/tree/main/example).
Or, you could run the example yourself:
```
git clone https://github.com/Iconica-Development/flutter_product_page.git
cd flutter_product_page
cd example
flutter run
```
## Issues
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_product_page) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
## Want to contribute
If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_product_page/pulls).
## Author
This flutter_product_page for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>

View file

@ -1,9 +0,0 @@
include: package:flutter_iconica_analysis/components_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -1,8 +0,0 @@
/// Module for creating a product page with a list of products and a
/// detailed view of each product.
library flutter_product_page;
export "src/configuration/product_page_configuration.dart";
export "src/configuration/product_page_shop_selector_style.dart";
export "src/configuration/product_page_translations.dart";
export "src/product_page_screen.dart";

View file

@ -1,67 +0,0 @@
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,173 +0,0 @@
import "package:flutter/material.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,
required this.getProductsInShoppingCart,
this.shoppingCartButtonBuilder,
this.initialShopId,
this.productBuilder,
this.onShopSelectionChange,
this.translations = const ProductPageTranslations(),
this.shopSelectorStyle = ShopSelectorStyle.row,
this.pagePadding = const EdgeInsets.all(4),
this.appBarBuilder,
this.bottomNavigationBar,
this.onProductDetail,
this.discountDescription,
this.noContentBuilder,
this.errorBuilder,
this.shopselectorBuilder,
this.discountBuilder,
this.categoryListBuilder,
this.selectedCategoryBuilder,
}) {
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<Shop>> Function() shops;
/// A function that returns all the products that belong to a certain shop.
/// The function must return a [List<Product>].
final Future<List<Product>> Function(Shop shop) getProducts;
/// The localizations for the product page.
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.
final Widget Function(
BuildContext context,
Product product,
ProductPageConfiguration configuration,
)? productBuilder;
/// 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.
final Widget Function(
BuildContext context,
ProductPageConfiguration configuration,
)? shoppingCartButtonBuilder;
/// The function that returns the discount description for a product.
String Function(
Product product,
)? discountDescription;
/// This function must be implemented by the developer and should handle the
/// adding of a product to the cart.
Function(Product product) onAddToCart;
/// This function gets executed when the user changes the shop selection.
/// This function always fires upon first load with the initial shop as well.
final Function(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;
/// This function must be implemented by the developer and should handle the
/// navigation to the shopping cart overview page.
final Function() onNavigateToShoppingCart;
/// The style of the shop selector.
final ShopSelectorStyle shopSelectorStyle;
/// The padding for the page.
final EdgeInsets pagePadding;
/// Optional app bar that you can pass to the product page screen.
final Widget? bottomNavigationBar;
/// Optional app bar that you can pass to the order detail screen.
final PreferredSizeWidget Function(BuildContext context)? appBarBuilder;
/// Builder for the no content widget. This builder is used when there is no
/// content to display.
final Widget Function(
BuildContext context,
)? noContentBuilder;
/// Builder for the error widget. This builder is used when there is an error
/// to display.
final Widget Function(
BuildContext context,
Object? error,
)? errorBuilder;
/// Builder for the shop selector. This builder is used to build the shop
/// selector that will be displayed in the product page.
final Widget Function(
BuildContext context,
ProductPageConfiguration configuration,
List<Shop> shops,
Function(Shop shop) onShopSelectionChange,
)? shopselectorBuilder;
/// Builder for the discount widget. This builder is used to build the
/// discount widget that will be displayed in the product page.
final Widget Function(
BuildContext context,
ProductPageConfiguration configuration,
List<Product> discountedProducts,
)? discountBuilder;
/// Builder for the list of items that are displayed in the product page.
final Widget Function(
BuildContext context,
ProductPageConfiguration configuration,
List<Product> products,
)? categoryListBuilder;
/// Builder for the list of selected categories
final Widget Function(ProductPageConfiguration configuration)?
selectedCategoryBuilder;
}
Future<void> _onProductDetail(
BuildContext context,
Product product,
String closeText,
) async {
var theme = Theme.of(context);
await showModalBottomSheet(
context: context,
backgroundColor: theme.colorScheme.surface,
builder: (context) => ProductItemPopup(
product: product,
closeText: closeText,
),
);
}
String _defaultDiscountDescription(
Product product,
) =>
"${product.name}, now for ${product.discountPrice} each";

View file

@ -1,8 +0,0 @@
/// Style for the shop selector in the product page.
enum ShopSelectorStyle {
/// Shops are displayed in a row.
row,
/// Shops are displayed in a wrap.
spacedWrap,
}

View file

@ -1,30 +0,0 @@
/// Localization for the product page
class ProductPageTranslations {
/// Default constructor
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
final String navigateToShoppingCart;
/// Title for the discount
final String discountTitle;
/// Explenation when the image failed to load
final String failedToLoadImageExplenation;
/// 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,306 +0,0 @@
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,60 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_nested_categories/flutter_nested_categories.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_product_page/src/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,
List<Product> products,
) {
var theme = Theme.of(context);
var categorizedProducts = <String, List<Product>>{};
for (var product in products) {
if (!categorizedProducts.containsKey(product.category)) {
categorizedProducts[product.category] = [];
}
categorizedProducts[product.category]?.add(product);
}
// Create Category instances
var categories = <Category>[];
categorizedProducts.forEach((categoryName, productList) {
var productWidgets = productList
.map(
(product) =>
configuration.productBuilder
?.call(context, product, configuration) ??
DefaultProductItem(
product: product,
onAddToCart: configuration.onAddToCart,
onProductDetail: configuration.onProductDetail!,
translations: configuration.translations,
),
)
.toList();
var category = Category(
name: categoryName,
content: productWidgets,
);
categories.add(category);
});
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (var category in categories) ...[
Text(
category.name!,
style: theme.textTheme.titleMedium,
),
Column(
children: category.content,
),
const SizedBox(height: 16),
],
],
);
}

View file

@ -1,45 +0,0 @@
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

@ -1,24 +0,0 @@
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

@ -1,18 +0,0 @@
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,191 +0,0 @@
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
import "package:skeletonizer/skeletonizer.dart";
/// Product item widget.
class DefaultProductItem extends StatelessWidget {
/// Constructor for the product item widget.
const DefaultProductItem({
required this.product,
required this.onProductDetail,
required this.onAddToCart,
required this.translations,
super.key,
});
/// Product to display.
final Product product;
/// Function to call when the product detail is requested.
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 ProductPageTranslations translations;
/// Size of the product image.
static const double imageSize = 44;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var loadingImageSkeleton = const Skeletonizer.zone(
child: SizedBox(width: imageSize, height: imageSize, child: Bone.icon()),
);
var productIcon = ClipRRect(
borderRadius: BorderRadius.circular(4),
child: CachedNetworkImage(
imageUrl: product.imageUrl,
width: imageSize,
height: imageSize,
fit: BoxFit.cover,
placeholder: (context, url) => loadingImageSkeleton,
errorWidget: (context, url, error) => Tooltip(
message: translations.failedToLoadImageExplenation,
child: Container(
width: 48,
height: 48,
alignment: Alignment.center,
child: const Icon(
Icons.error_outline_sharp,
color: Colors.red,
),
),
),
),
);
var productName = Padding(
padding: const EdgeInsets.only(left: 8),
child: Container(
constraints: const BoxConstraints(maxWidth: 150),
child: Text(
product.name,
style: theme.textTheme.titleMedium,
),
),
);
var productInformationIcon = Padding(
padding: const EdgeInsets.only(left: 4),
child: IconButton(
onPressed: () => onProductDetail(
context,
product,
translations.close,
),
icon: Icon(
Icons.info_outline,
color: theme.colorScheme.primary,
),
),
);
var productInteraction = Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_PriceLabel(
product: product,
),
_AddToCardButton(
product: product,
onAddToCart: onAddToCart,
),
],
);
return Padding(
padding: const EdgeInsets.only(top: 12, bottom: 8),
child: Row(
children: [
productIcon,
productName,
productInformationIcon,
const Spacer(),
productInteraction,
],
),
);
}
}
class _PriceLabel extends StatelessWidget {
const _PriceLabel({
required this.product,
});
final Product product;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Row(
children: [
if (product.hasDiscount) ...[
Text(
product.price.toStringAsFixed(2),
style: theme.textTheme.bodySmall?.copyWith(
decoration: TextDecoration.lineThrough,
),
textAlign: TextAlign.center,
),
const SizedBox(width: 4),
],
Text(
product.hasDiscount
? product.discountPrice!.toStringAsFixed(2)
: product.price.toStringAsFixed(2),
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
],
);
}
}
class _AddToCardButton extends StatelessWidget {
const _AddToCardButton({
required this.product,
required this.onAddToCart,
});
final Product product;
final Function(Product product) onAddToCart;
static const double boxSize = 29;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.primary,
borderRadius: BorderRadius.circular(4),
),
width: boxSize,
height: boxSize,
child: Center(
child: IconButton(
padding: EdgeInsets.zero,
icon: const Icon(
Icons.add,
color: Colors.white,
size: 20,
),
onPressed: () => onAddToCart(product),
),
),
);
}
}

View file

@ -1,71 +0,0 @@
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

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

View file

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

View file

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

View file

@ -1,123 +0,0 @@
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart";
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// A widget that displays a weekly discount.
class WeeklyDiscount extends StatelessWidget {
/// Creates a weekly discount.
const WeeklyDiscount({
required this.configuration,
required this.product,
super.key,
});
/// Configuration for the product page.
final ProductPageConfiguration configuration;
/// The product for which the discount is displayed.
final Product product;
/// The top padding of the widget.
static const double topPadding = 20;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var bottomText = Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
configuration.discountDescription!(product),
style: theme.textTheme.bodyMedium,
textAlign: TextAlign.left,
),
);
var loadingImage = const Padding(
padding: EdgeInsets.all(32.0),
child: Center(
child: CircularProgressIndicator.adaptive(),
),
);
var errorImage = Padding(
padding: const EdgeInsets.all(32.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline_rounded,
color: Colors.red,
),
Text(configuration.translations.failedToLoadImageExplenation),
],
),
),
);
var image = Padding(
padding: const EdgeInsets.symmetric(horizontal: 1.0),
child: AspectRatio(
aspectRatio: 1,
child: CachedNetworkImage(
imageUrl: product.imageUrl,
width: double.infinity,
fit: BoxFit.cover,
placeholder: (context, url) => loadingImage,
errorWidget: (context, url, error) => errorImage,
),
),
);
var topText = DecoratedBox(
decoration: const BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(4),
),
),
child: SizedBox(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
child: Text(
configuration.translations.discountTitle,
style: theme.textTheme.headlineSmall,
textAlign: TextAlign.left,
),
),
),
);
var boxDecoration = BoxDecoration(
border: Border.all(
width: 1.0,
),
borderRadius: BorderRadius.circular(4.0),
);
return Padding(
padding: const EdgeInsets.only(top: topPadding),
child: DecoratedBox(
decoration: boxDecoration,
child: SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
topText,
image,
bottomText,
],
),
),
),
);
}
}

View file

@ -1,35 +0,0 @@
name: flutter_product_page
description: "A Flutter module for the product page"
publish_to: "none"
version: 2.0.0
environment:
sdk: ">=3.3.4 <4.0.0"
dependencies:
flutter:
sdk: flutter
skeletonizer: ^1.1.1
cached_network_image: ^3.3.1
flutter_nested_categories:
git:
url: https://github.com/Iconica-Development/flutter_nested_categories
ref: 0.0.1
flutter_shopping_interface:
git:
url: https://github.com/Iconica-Development/flutter_shopping
path: packages/flutter_shopping_interface
ref: 2.0.0
collection: ^1.18.0
provider: ^6.1.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_iconica_analysis:
git:
url: https://github.com/Iconica-Development/flutter_iconica_analysis
ref: 7.0.0
flutter:
uses-material-design: true

View file

@ -9,7 +9,6 @@
.history
.svn/
migrate_working_dir/
.metadata
# IntelliJ related
*.iml
@ -20,37 +19,11 @@ migrate_working_dir/
# 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/
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
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
# env
*dotenv
android/
ios/
web/
macos/
windows/
linux/
build/

View file

@ -1,181 +1,39 @@
# flutter_shopping
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
The flutter_shopping user-story allows you to create an user shopping flow within minutes of work. This user-story contains the ability to show products, shopping cart, gathering user information and order succes and failed screens.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/tools/pub/writing-package-pages).
## Setup
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/to/develop-packages).
-->
(1) Set up your `MyShop` model by extending from the `ProductPageShop` class. The most basic version looks like this:
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
```dart
class MyShop extends ProductPageShop {
const MyShop({
required super.id,
required super.name,
});
}
```
## Features
(2) Set up your `MyProduct` model by extending from `ShoppingCartProduct` and extending from the mixin `ProductPageProduct`, like this:
TODO: List what your package can do. Maybe include images, gifs, or videos.
```dart
class MyProduct extends ShoppingCartProduct with ProductPageProduct {
MyProduct({
required super.id,
required super.name,
required super.price,
required this.category,
required this.imageUrl,
this.discountPrice,
this.hasDiscount = false,
});
## Getting started
@override
final String category;
@override
final String imageUrl;
@override
final double? discountPrice;
@override
final bool hasDiscount;
}
```
(3) Finally in your `routes.dart` import all the routes from the user-story:
```dart
...getShoppingStoryRoutes(
configuration: ...
),
```
(4) Create a new instantiation of the ProductService class:
```dart
final ProductService<MyProduct> productService = ProductService([]);
```
(5) Set up the `FlutterShoppingConfiguration`:
```dart
FlutterShoppingConfiguration(
// (REQUIRED): Shop builder configuration
shopBuilder: (BuildContext context) => ProductPageScreen(
configuration: ProductPageConfiguration(
// (REQUIRED): List of shops that should be displayed
// If there is only one, make a list with just one shop.
shops: getShops(),
// (REQUIRED): Function to add a product to the cart
onAddToCart: (ProductPageProduct product) =>
productService.addProduct(product as MyProduct),
// (REQUIRED): Function to get the products for a shop
getProducts: (String shopId) => Future<ProductPageContent>.value(
getShopContent(shopId),
),
// (REQUIRED): Function to navigate to the shopping cart
onNavigateToShoppingCart: () => onCompleteProductPage(context),
// (RECOMMENDED) Function that returns the description for a
// product that is on sale.
getDiscountDescription: (ProductPageProduct product) =>
"""${product.name} for just \$${product.discountPrice?.toStringAsFixed(2)}""",
// (RECOMMENDED) Function that is fired when the shop selection
// changes. You could use this to clear your shopping cart or to
// change the products so they belong to the correct shop again.
onShopSelectionChange: (ProductPageShop shop) => productService.clear(),
// (RECOMMENDED) The shop that is initially selected.
// Must be one of the shops in the [shops] list.
initialShop: getShops().first,
// (RECOMMENDED) Localizations for the product page.
localizations: const ProductPageLocalization(),
// (OPTIONAL) Appbar
appBar: ...
),
),
// (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) => ...
// (OPTIONAL/REQUIRED) on confirm order callback:
// Either use this callback or the placeOrderButtonBuilder.
onConfirmOrder: (products) => onCompleteShoppingCart(context),
// (RECOMMENDED) localizations:
localizations: const ShoppingCartLocalizations(),
// (OPTIONAL) custom appbar:
appBar: ...
),
),
// (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 {
...
},
)
```
For more information about the component specific items please take a look at their repositories:
- [flutter_product_page](https://github.com/Iconica-Development/flutter_product_page/)
- [flutter_shopping_cart](https://github.com/Iconica-Development/flutter_shopping_cart)
- [flutter_order_details](https://github.com/Iconica-Development/flutter_order_details)
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
For a detailed example you can see the [example](https://github.com/Iconica-Development/flutter_shopping/tree/main/example).
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
Or, you could run the example yourself:
```
git clone https://github.com/Iconica-Development/flutter_shopping.git
cd flutter_shopping
cd example
flutter run
```dart
const like = 'sample';
```
## Issues
## Additional information
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_shopping) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
## Want to contribute
If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_shopping/pulls).
## Author
This flutter_shopping for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

View file

@ -9,7 +9,6 @@
.history
.svn/
migrate_working_dir/
.metadata
# IntelliJ related
*.iml
@ -20,7 +19,7 @@ migrate_working_dir/
# 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/
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
@ -28,11 +27,9 @@ migrate_working_dir/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
pubspec.lock
# Symbolication related
app.*.symbols
@ -44,6 +41,3 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# env
*dotenv

View file

@ -0,0 +1,28 @@
# 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

@ -0,0 +1,30 @@
import 'package:example/theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_shopping/flutter_shopping.dart';
void main(List<String> args) {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: theme,
home: const Home(),
);
}
}
class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) {
return const FlutterShoppingNavigatorUserstory(
options: FlutterShoppingOptions(),
);
}
}

View file

@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
const Color primaryColor = Color(0xFF71C6D1);
ThemeData theme = ThemeData(
actionIconTheme: ActionIconThemeData(
backButtonIconBuilder: (context) {
return const Icon(Icons.arrow_back_ios_new_rounded);
},
),
scaffoldBackgroundColor: const Color(0xFFFAF9F6),
primaryColor: primaryColor,
checkboxTheme: CheckboxThemeData(
side: const BorderSide(
color: Color(0xFF8D8D8D),
width: 1,
),
fillColor: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return primaryColor;
}
return const Color(0xFFEEEEEE);
},
),
),
switchTheme: SwitchThemeData(
trackColor:
WidgetStateProperty.resolveWith<Color>((Set<WidgetState> states) {
if (!states.contains(WidgetState.selected)) {
return const Color(0xFF8D8D8D);
}
return primaryColor;
}),
thumbColor: const WidgetStatePropertyAll(
Colors.white,
),
),
appBarTheme: const AppBarTheme(
centerTitle: true,
iconTheme: IconThemeData(
color: Colors.white,
size: 16,
),
elevation: 0,
backgroundColor: Color(0xFF212121),
titleTextStyle: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 24,
color: Color(0xFF71C6D1),
fontFamily: "Merriweather",
),
actionsIconTheme: IconThemeData()),
fontFamily: "Merriweather",
useMaterial3: false,
textTheme: const TextTheme(
headlineSmall: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 16,
color: Colors.white,
),
headlineLarge: TextStyle(
fontWeight: FontWeight.w900,
fontSize: 24,
color: Color(0xFF71C6D1),
),
displayLarge: TextStyle(
fontFamily: "Avenir",
fontWeight: FontWeight.w800,
fontSize: 20,
color: Colors.white,
),
displayMedium: TextStyle(
fontFamily: "Avenir",
fontWeight: FontWeight.w800,
fontSize: 18,
color: Color(0xFF71C6D1),
),
displaySmall: TextStyle(
fontFamily: "Avenir",
fontWeight: FontWeight.w800,
fontSize: 14,
color: Colors.black,
),
// TITLE
titleSmall: TextStyle(
fontFamily: "Avenir",
fontWeight: FontWeight.w800,
fontSize: 14,
color: Colors.white,
),
titleMedium: TextStyle(
fontFamily: "Avenir",
fontWeight: FontWeight.w800,
fontSize: 16,
color: Colors.black,
),
titleLarge: TextStyle(
fontFamily: "Avenir",
fontWeight: FontWeight.w800,
fontSize: 20,
color: Colors.black,
),
// LABEL
labelSmall: TextStyle(
fontFamily: "Avenir",
fontWeight: FontWeight.w400,
fontSize: 12,
color: Color(0xFF8D8D8D),
),
// BODY
bodySmall: TextStyle(
fontFamily: "Avenir",
fontWeight: FontWeight.w400,
fontSize: 14,
color: Colors.black,
),
bodyMedium: TextStyle(
fontFamily: "Avenir",
fontWeight: FontWeight.w400,
fontSize: 16,
color: Colors.black,
),
),
radioTheme: RadioThemeData(
visualDensity: const VisualDensity(
horizontal: 0,
vertical: -2,
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
fillColor: WidgetStateProperty.resolveWith<Color>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return primaryColor;
}
return Colors.black;
},
),
),
colorScheme: const ColorScheme.light(
primary: primaryColor,
),
);

View file

@ -0,0 +1,34 @@
name: example
description: "A new Flutter project."
publish_to: "none"
version: 1.0.0+1
environment:
sdk: ^3.5.1
dependencies:
flutter:
sdk: flutter
flutter_shopping:
path: ../
shopping_repository_interface:
path: ../../shopping_repository_interface
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
flutter:
uses-material-design: true
fonts:
- family: Merriweather
fonts:
- asset: fonts/Merriweather-Regular.ttf
- family: Avenir
fonts:
- asset: fonts/Avenir-Regular.ttf

View file

@ -1,9 +1,30 @@
/// Flutter Shopping
library flutter_shopping;
/// userstory
// ignore_for_file: directives_ordering
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";
library;
export "src/configuration/shopping_configuration.dart";
export "package:shopping_repository_interface/shopping_repository_interface.dart";
export "src/flutter_shopping_navigator_userstory.dart";
/// screens
export "src/screens/shopping_screen.dart";
export "src/screens/shopping_cart_screen.dart";
export "src/screens/filter_screen.dart";
export "src/screens/personal_information_screen.dart";
export "src/screens/date_time_information_screen.dart";
export "src/screens/payment_options_screen.dart";
/// models
export "src/models/shopping_translations.dart";
export "src/models/flutter_shopping_options.dart";
/// widgets
export "src/widgets/primary_button.dart";
export "src/widgets/shop_selector.dart";
export "src/widgets/filter_item.dart";
export "src/widgets/shopping_cart_item.dart";
export "src/widgets/info_bottomsheet.dart";
export "src/widgets/product_item_list.dart";
export "src/widgets/product_item.dart";
export "src/widgets/shop_selector_item.dart";
export "src/widgets/weekly_offer.dart";

View file

@ -1,240 +0,0 @@
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

@ -1,235 +1,109 @@
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,
/// A userstory for the FlutterShoppingNavigator.
class FlutterShoppingNavigatorUserstory extends StatefulWidget {
/// Constructor for the FlutterShoppingNavigatorUserstory.
const FlutterShoppingNavigatorUserstory({
this.options = const FlutterShoppingOptions(),
this.translations = const ShoppingTranslations(),
this.shoppingService,
this.initialShopId,
super.key,
});
/// Shopping configuration.
final ShoppingConfiguration? shoppingConfiguration;
/// The options for the shopping navigator.
final FlutterShoppingOptions options;
/// The shopping service.
final ShoppingService? shoppingService;
/// The translations.
final ShoppingTranslations translations;
/// The initial shop id.
final String? initialShopId;
@override
Widget build(BuildContext context) => ShoppingProductPage(
shoppingConfiguration: shoppingConfiguration ??
ShoppingConfiguration(
shoppingService: LocalShoppingService(),
),
);
State<FlutterShoppingNavigatorUserstory> createState() =>
_FlutterShoppingNavigatorUserstoryState();
}
/// 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;
class _FlutterShoppingNavigatorUserstoryState
extends State<FlutterShoppingNavigatorUserstory> {
late ShoppingService shoppingService;
@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();
void initState() {
shoppingService = widget.shoppingService ?? ShoppingService();
super.initState();
}
},
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,
),
),
);
}
Widget build(BuildContext context) => shoppingScreen();
Widget shoppingScreen() => ShoppingScreen(
initialShopId: widget.initialShopId,
translations: widget.translations,
shoppingService: shoppingService,
options: widget.options,
onFilterPressed: () async {
widget.options.onFilterPressed?.call() ?? await push(filterScreen());
},
onShoppingCartPressed: () async {
widget.options.onCartPressed?.call() ??
await push(shoppingCartScreen());
},
),
);
Widget filterScreen() => FilterScreen(
translations: widget.translations,
shoppingService: shoppingService,
options: widget.options,
);
Widget shoppingCartScreen() => ShoppingCartScreen(
translations: widget.translations,
shoppingService: shoppingService,
options: widget.options,
onOrder: () async {
widget.options.onOrderPressed?.call() ??
await push(personalInformationScreen());
},
);
Widget personalInformationScreen() => PersonalInformationScreen(
translations: widget.translations,
options: widget.options,
onFinished: (value) async {
widget.options.getPersonalInformation?.call(value);
widget.options.onPersonalInformationPressed?.call() ??
await push(dateTimeInformationScreen());
},
);
Widget dateTimeInformationScreen() => DateTimeInformationScreen(
translations: widget.translations,
onFinished: (value) async {
widget.options.getDateTimeInformation?.call(value);
widget.options.onDateTimePressed?.call() ??
await push(paymentOptionsScreen());
},
);
Widget paymentOptionsScreen() => PaymentOptionsScreen(
translations: widget.translations,
onFinished: (value) async {
widget.options.getPaymentInformation?.call(value);
widget.options.onPaymentPressed?.call() ?? await popUntil();
},
);
Future<void> push(Widget screen) async {
await Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => screen));
}
Future<void> popUntil() async {
Navigator.of(context).popUntil((route) => route.isFirst);
}
}
/// 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

@ -0,0 +1,132 @@
import "package:flutter/material.dart";
import "package:shopping_repository_interface/shopping_repository_interface.dart";
/// The options for the flutter shopping package.
class FlutterShoppingOptions {
/// The constructor for the FlutterShoppingOptions.
const FlutterShoppingOptions({
this.shoppingScreenAppbarBuilder,
this.filterChipbuilder,
this.weeklyOfferBuilder,
this.productItemBuilder,
this.primaryButtonBuilder,
this.shopSelectorItemBuilder,
this.filterScreenAppbarBuilder,
this.filterItemBuilder,
this.shoppingCartItemBuilder,
this.onInfoPressed,
this.informationInputDecoration,
this.onFilterPressed,
this.onCartPressed,
this.onOrderPressed,
this.onPersonalInformationPressed,
this.onDateTimePressed,
this.onPaymentPressed,
this.shoppingScreenDrawer,
this.getPersonalInformation,
this.getDateTimeInformation,
this.getPaymentInformation,
});
/// The appbar builder for the shopping screen.
final AppBarBuilder? shoppingScreenAppbarBuilder;
/// The weekly offer builder for the shopping screen.
final ItemBuilder? weeklyOfferBuilder;
/// The product item builder for the shopping screen.
final ItemBuilder? productItemBuilder;
/// The filter chip builder for the shopping screen.
final Widget? Function(
BuildContext context,
String title,
Function() onDelete,
)? filterChipbuilder;
/// The primary button builder.
final Widget Function(
BuildContext context,
// ignore: avoid_positional_boolean_parameters
bool enabled,
String title,
Function() onPressed,
)? primaryButtonBuilder;
/// shop selector item builder.
final Widget Function(
BuildContext context,
Shop shop,
Function(Shop) onSelected,
)? shopSelectorItemBuilder;
/// The appbar builder for the filter screen.
final AppBarBuilder? filterScreenAppbarBuilder;
/// The filter item builder.
final Widget Function(
BuildContext context,
Function() onDeselect,
Function() onSelect,
// ignore: avoid_positional_boolean_parameters
bool currentValue,
)? filterItemBuilder;
/// The shopping cart item builder.
final Widget Function(
BuildContext context,
Product product,
Function() onAddToCart,
Function() onRemoveFromCart,
)? shoppingCartItemBuilder;
/// The on info pressed function.
final Function(Product product)? onInfoPressed;
/// The input decoration for the information.
final InputDecoration? Function(BuildContext context, String hintText)?
informationInputDecoration;
/// on filter pressed.
final Function()? onFilterPressed;
/// on cart pressed.
final Function()? onCartPressed;
/// on order pressed.
final Function()? onOrderPressed;
/// on personal information pressed.
final Function()? onPersonalInformationPressed;
/// on address pressed.
final Function()? onDateTimePressed;
/// on payment pressed.
final Function()? onPaymentPressed;
/// The shopping screen drawer.
final Widget? shoppingScreenDrawer;
/// The function to get the personal information.
final Function(Map<String, dynamic> value)? getPersonalInformation;
/// The function to get the address information.
final Function(Map<String, dynamic> value)? getDateTimeInformation;
/// The function to get the payment information.
final Function(Map<String, dynamic> value)? getPaymentInformation;
}
/// The appbar builder.
typedef AppBarBuilder = AppBar Function(
BuildContext context,
String title,
Function()? onFilterPressed,
);
/// The item builder.
typedef ItemBuilder = Widget Function(
BuildContext context,
Product product,
);

View file

@ -0,0 +1,225 @@
/// Translations for the Shopping package.
class ShoppingTranslations {
/// Constructor for the Shopping translations.
const ShoppingTranslations({
/// screen titles
this.filterTitle = "filter",
this.shoppingCartTitle = "Shopping cart",
this.personalInformationTitle = "information",
this.dateTimeInformationTitle = "information",
this.paymentOptionsTitle = "information",
/// button text
this.orderButton = "Order",
this.closeInfo = "Close",
this.viewShoppingCart = "View shopping cart",
this.personalInformationButton = "Choose date and time",
this.paymentButton = "Next",
this.datetimeInformationButton = "Next",
/// body text
this.weeklyOffer = "Weekly offer",
this.whatWouldyouLikeToOrder = "What would you like to order?",
this.shoppingCartProducts = "Products",
this.shoppingCartTotal = "Subtotal",
this.shoppingCartCurrency = "",
this.personalInformationName = "Whats your name?",
this.personalInformationAddress = "Whats your address?",
this.personalInformationPhone = "Whats your phone number?",
this.personalInformationEmail = "Whats your email address?",
this.personalInformationComment = "Do you have any comments?",
this.paymenttitle = "Payment method",
this.paymentExplainer =
"Choose when you would like to to pay for the order.",
this.payNow = "Pay now",
this.payLater = "Pay later",
this.chooseDateAndTime =
"When and at what time would you like to pick up your order?",
this.selectDay = "Select a day",
this.dayToday = "Today",
this.dayTomorrow = "Tomorrow",
this.morning = "Morning",
this.afternoon = "Afternoon",
this.selectTime = "Select a time",
/// error messages
this.nameRequired = "Name is required",
this.addressRequired = "Please enter a street and house number",
this.postalCodeRequired = "Please enter your postal code",
this.cityRequired = "Please enter your city",
this.phoneRequired = "Please enter your phone number",
this.emailRequired = "Email is required",
this.invalidEmail = "Please fill in a valid email address",
this.paymentError = "Please select a payment method",
this.invalidAdress = "Invalid street and house number",
this.invalidPostalCode = "Invalid postal code",
this.invalidPhoneLength = "Invalid phone number length",
this.phoneContainsLettersError = "Phone number can only contain digits",
this.selectDayError = "Please select a day",
/// hint text
this.nameHint = "full name",
this.addressHint = "street name and number",
this.postalCodeHint = "postal code",
this.cityHint = "city",
this.phoneHint = "phone number",
this.emailHint = "email address",
this.commentHint = "optional",
});
/// Appbar title for filter screen.
final String filterTitle;
/// Appbar title for shopping cart screen.
final String shoppingCartTitle;
/// Appbar title for personal information screen.
final String personalInformationTitle;
/// Text for the order button.
final String orderButton;
/// Text for the close info button.
final String closeInfo;
/// Text for the view shopping cart button.
final String viewShoppingCart;
/// Text for the personal information button.
final String personalInformationButton;
/// Text for the weekly offer.
final String weeklyOffer;
/// Text for the question what would you like to order.
final String whatWouldyouLikeToOrder;
/// Text for the products.
final String shoppingCartProducts;
/// Text for the total price in the shopping cart.
final String shoppingCartTotal;
/// The currency that is being used
final String shoppingCartCurrency;
/// Text for the personal information fields.
final String personalInformationName;
/// Text for the personal information fields.
final String personalInformationAddress;
/// Text for the personal information fields.
final String personalInformationPhone;
/// Text for the personal information fields.
final String personalInformationEmail;
/// Text for the personal information fields.
final String personalInformationComment;
/// Text for the name required field.
final String nameRequired;
/// Text for the address field.
final String addressRequired;
/// Text for the postal code field.
final String postalCodeRequired;
/// Text for the city field.
final String cityRequired;
/// Text for the phone field.
final String phoneRequired;
/// Text for the email field.
final String emailRequired;
/// Text for the invalid email field.
final String invalidEmail;
/// Text for the name hint.
final String nameHint;
/// Text for the address hint.
final String addressHint;
/// Text for the postal code hint.
final String postalCodeHint;
/// Text for the city hint.
final String cityHint;
/// Text for the phone hint.
final String phoneHint;
/// Text for the email hint.
final String emailHint;
/// Text for the comment hint.
final String commentHint;
/// Text for the date and time information title.
final String dateTimeInformationTitle;
/// Text for the payment options title.
final String paymentOptionsTitle;
/// Text for the payment error.
final String paymentError;
/// Text for the payment title.
final String paymenttitle;
/// Text for the payment explainer.
final String paymentExplainer;
/// Text for the pay now button.
final String payNow;
/// Text for the pay later button.
final String payLater;
/// Text for the payment button.
final String paymentButton;
/// Text for the invalid address.
final String invalidAdress;
/// Text for the invalid postal code.
final String invalidPostalCode;
/// Text for the invalid phone length.
final String invalidPhoneLength;
/// Text for the phone contains letters error.
final String phoneContainsLettersError;
/// Text for the select day error.
final String datetimeInformationButton;
/// Text for the choose date and time.
final String chooseDateAndTime;
/// Text for the select day.
final String selectDay;
/// Text for the select day error.
final String selectDayError;
/// Text for the day today.
final String dayToday;
/// Text for the day tomorrow.
final String dayTomorrow;
/// Text for the morning.
final String morning;
/// Text for the afternoon.
final String afternoon;
/// Text for the select time.
final String selectTime;
}

View file

@ -0,0 +1,257 @@
import "package:animated_toggle/animated_toggle.dart";
import "package:flutter/material.dart";
import "package:flutter_form_wizard/flutter_form.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// The morning times.
List<String> 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",
];
/// The afternoon times.
List<String> 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",
];
/// The personal information page.
class DateTimeInformationScreen extends StatefulWidget {
/// Constructor for the PersonalInformationScreen.
const DateTimeInformationScreen({
required this.translations,
required this.onFinished,
super.key,
});
/// The translations.
final ShoppingTranslations translations;
/// The on finished function.
final Function(Map<String, dynamic>) onFinished;
@override
State<DateTimeInformationScreen> createState() =>
_DateTimeInformationScreenState();
}
class _DateTimeInformationScreenState extends State<DateTimeInformationScreen> {
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var dateTimeInformationController = FlutterFormController();
var multipleChoiceController = FlutterFormInputMultipleChoiceController(
id: "multipleChoice",
mandatory: true,
);
InputDecoration dropdownInputDecoration(String hint) => InputDecoration(
hintStyle: theme.textTheme.bodySmall,
hintText: hint,
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
);
var switchStatus = ValueNotifier(false);
return Scaffold(
appBar: AppBar(
title: Text(widget.translations.dateTimeInformationTitle),
),
body: FlutterForm(
formController: dateTimeInformationController,
options: FlutterFormOptions(
pages: [
FlutterFormPage(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Text(
widget.translations.chooseDateAndTime,
style: theme.textTheme.titleMedium,
),
),
const SizedBox(
height: 4,
),
FlutterFormInputDropdown(
icon: const Icon(
Icons.keyboard_arrow_down,
color: Colors.black,
),
isDense: true,
decoration: dropdownInputDecoration(
widget.translations.selectDay,
),
validationMessage: widget.translations.selectDayError,
controller: FlutterFormInputDropdownController(
id: "date",
mandatory: true,
),
items: [
DropdownMenuItem(
value: widget.translations.dayToday,
child: Text(
widget.translations.dayToday,
style: theme.textTheme.bodySmall,
),
),
DropdownMenuItem(
value: widget.translations.dayTomorrow,
child: Text(
widget.translations.dayTomorrow,
style: theme.textTheme.bodySmall,
),
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: ValueListenableBuilder(
valueListenable: switchStatus,
builder: (context, value, 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 = !switchStatus.value;
},
childLeft: Center(
child: Text(
widget.translations.morning,
style: theme.textTheme.titleSmall?.copyWith(
color: switchStatus.value
? theme.colorScheme.primary
: Colors.white,
),
),
),
childRight: Center(
child: Text(
widget.translations.afternoon,
style: theme.textTheme.titleSmall?.copyWith(
color: switchStatus.value
? Colors.white
: theme.colorScheme.primary,
),
),
),
),
),
),
const SizedBox(
height: 8,
),
ValueListenableBuilder(
valueListenable: switchStatus,
builder: (context, value, child) =>
FlutterFormInputMultipleChoice(
validationMessage: widget.translations.selectTime,
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]),
),
),
),
),
),
],
),
),
),
],
nextButton: (pageNumber, checkingPages) => Padding(
padding: const EdgeInsets.only(bottom: 32),
child: Align(
alignment: Alignment.bottomCenter,
child: PrimaryButton(
text: widget.translations.personalInformationButton,
onPressed: () async {
await dateTimeInformationController.autoNextStep();
},
),
),
),
onFinished: (values) {
widget.onFinished(values.entries.first.value);
},
onNext: (page, values) {},
),
),
);
}
}

View file

@ -0,0 +1,106 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
import "package:rxdart/rxdart.dart";
/// filter screen
class FilterScreen extends StatelessWidget {
/// Constructor for the FilterScreen.
const FilterScreen({
required this.shoppingService,
required this.translations,
required this.options,
super.key,
});
/// The shopping service.
final ShoppingService shoppingService;
/// The translations.
final ShoppingTranslations translations;
/// The options.
final FlutterShoppingOptions options;
@override
Widget build(BuildContext context) => Scaffold(
appBar: options.filterScreenAppbarBuilder?.call(
context,
translations.filterTitle,
() {
Navigator.of(context).pop();
},
) ??
AppBar(
leading: const SizedBox.shrink(),
title: Text(translations.filterTitle),
actions: [
IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(
Icons.close_rounded,
size: 28,
),
),
],
),
body: SingleChildScrollView(
child: StreamBuilder<List<dynamic>>(
stream: Rx.combineLatest(
[
shoppingService.getCategories(),
shoppingService.getSelectedCategoryStream(),
],
(List<dynamic> data) => data,
),
builder: (context, snapshot) {
if (snapshot.hasData) {
var categories = snapshot.data![0] as List<Category>;
var selectedCategories = snapshot.data![1] as List<Category>;
return Column(
children: [
...categories.map(
(category) {
var isSelected = containsCategoryById(
selectedCategories,
category.id,
);
return options.filterItemBuilder?.call(
context,
() {
shoppingService.deselectCategory(category.id);
},
() {
shoppingService.selectCategory(category.id);
},
isSelected,
) ??
FilterItem(
value: isSelected,
onChanged: (value) {
if (value) {
shoppingService.selectCategory(category.id);
} else {
shoppingService.deselectCategory(category.id);
}
},
category: category,
);
},
),
],
);
} else {
return const SizedBox.shrink();
}
},
),
),
);
}
/// Check if the category is selected.
bool containsCategoryById(List<Category> categories, String id) =>
categories.any((category) => category.id == id);

View file

@ -0,0 +1,118 @@
import "package:flutter/material.dart";
import "package:flutter_form_wizard/flutter_form.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// A screen for the payment options.
class PaymentOptionsScreen extends StatelessWidget {
/// Constructor for the PaymentOptionsScreen.
const PaymentOptionsScreen({
required this.translations,
required this.onFinished,
super.key,
});
/// The translations.
final ShoppingTranslations translations;
/// The on finished function.
final Function(Map<String, dynamic>) onFinished;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var paymentOptionsController = FlutterFormController();
return Scaffold(
appBar: AppBar(
title: Text(translations.paymentOptionsTitle),
),
body: FlutterForm(
formController: paymentOptionsController,
options: FlutterFormOptions(
pages: [
FlutterFormPage(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
translations.paymenttitle,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
Text(
translations.paymentExplainer,
style: theme.textTheme.bodyMedium,
),
const SizedBox(
height: 84,
),
FlutterFormInputMultipleChoice(
crossAxisCount: 1,
mainAxisSpacing: 24,
crossAxisSpacing: 5,
childAspectRatio: 2,
height: 422,
controller: FlutterFormInputMultipleChoiceController(
id: "payment",
mandatory: true,
),
options: [translations.payNow, translations.payLater],
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: translations.paymentError,
),
],
),
),
),
],
nextButton: (pageNumber, checkingPages) => Padding(
padding: const EdgeInsets.only(bottom: 32),
child: Align(
alignment: Alignment.bottomCenter,
child: PrimaryButton(
text: translations.paymentButton,
onPressed: () async {
await paymentOptionsController.autoNextStep();
},
),
),
),
onFinished: (values) {
onFinished(values.entries.first.value);
},
onNext: (page, values) {},
),
),
);
}
}

View file

@ -0,0 +1,256 @@
import "package:flutter/material.dart";
import "package:flutter_form_wizard/flutter_form.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// The personal information page.
class PersonalInformationScreen extends StatelessWidget {
/// Constructor for the PersonalInformationScreen.
const PersonalInformationScreen({
required this.translations,
required this.onFinished,
required this.options,
super.key,
});
/// The translations.
final ShoppingTranslations translations;
/// The on finished function.
final Function(Map<String, dynamic>) onFinished;
/// The options.
final FlutterShoppingOptions options;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var personalInforamtionController = FlutterFormController();
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,
),
);
return Scaffold(
appBar: AppBar(
title: Text(translations.personalInformationTitle),
),
body: FlutterForm(
formController: personalInforamtionController,
options: FlutterFormOptions(
pages: [
FlutterFormPage(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
translations.personalInformationName,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: options.informationInputDecoration
?.call(context, translations.nameHint) ??
inputDecoration(translations.nameHint),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "name",
mandatory: true,
),
validationMessage: translations.nameRequired,
),
const SizedBox(
height: 16,
),
Text(
translations.personalInformationAddress,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: options.informationInputDecoration
?.call(context, translations.addressHint) ??
inputDecoration(translations.addressHint),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "street",
mandatory: true,
),
validationMessage: translations.addressRequired,
validator: (value) {
if (value == null || value.isEmpty) {
return translations.addressRequired;
}
var regex = RegExp(r"^[A-Za-z]+\s[0-9]{1,3}$");
if (!regex.hasMatch(value)) {
return translations.invalidAdress;
}
return null;
},
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: options.informationInputDecoration
?.call(context, translations.postalCodeHint) ??
inputDecoration(translations.postalCodeHint),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "postalCode",
mandatory: true,
),
validationMessage: translations.postalCodeRequired,
validator: (value) {
if (value == null || value.isEmpty) {
return translations.postalCodeRequired;
}
var regex = RegExp(r"^[0-9]{4}[A-Za-z]{2}$");
if (!regex.hasMatch(value)) {
return translations.invalidPostalCode;
}
return null;
},
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: options.informationInputDecoration
?.call(context, translations.cityHint) ??
inputDecoration(translations.cityHint),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "city",
mandatory: true,
),
validationMessage: translations.cityRequired,
),
const SizedBox(
height: 16,
),
Text(
translations.personalInformationPhone,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputPhone(
numberFieldStyle: theme.textTheme.bodySmall,
textAlignVertical: TextAlignVertical.center,
decoration: options.informationInputDecoration
?.call(context, translations.phoneHint) ??
inputDecoration(translations.phoneHint),
controller: FlutterFormInputPhoneController(
id: "phone",
mandatory: true,
),
validationMessage: translations.phoneRequired,
validator: (value) {
if (value == null || value.number!.isEmpty) {
return translations.phoneRequired;
}
// 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 translations.invalidPhoneLength;
}
// Check if all remaining characters are digits
if (!phoneNumber
.substring(1)
.contains(RegExp(r"^[0-9]*$"))) {
return translations.phoneContainsLettersError;
}
// If all checks pass, return null (no error)
return null;
},
),
const SizedBox(
height: 16,
),
Text(
translations.personalInformationEmail,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputEmail(
style: theme.textTheme.bodySmall,
decoration: options.informationInputDecoration
?.call(context, translations.emailHint) ??
inputDecoration(translations.emailHint),
controller: FlutterFormInputEmailController(
id: "email",
mandatory: true,
),
validationMessage: translations.emailRequired,
),
const SizedBox(
height: 16,
),
Text(
translations.personalInformationComment,
style: theme.textTheme.titleMedium,
),
const SizedBox(
height: 4,
),
FlutterFormInputPlainText(
decoration: options.informationInputDecoration
?.call(context, translations.commentHint) ??
inputDecoration(translations.commentHint),
style: theme.textTheme.bodySmall,
controller: FlutterFormInputPlainTextController(
id: "comments",
),
validationMessage: "",
),
const SizedBox(
height: 100,
),
],
),
),
),
],
nextButton: (pageNumber, checkingPages) => Padding(
padding: const EdgeInsets.only(bottom: 32),
child: Align(
alignment: Alignment.bottomCenter,
child: PrimaryButton(
text: translations.personalInformationButton,
onPressed: () async {
await personalInforamtionController.autoNextStep();
},
),
),
),
onFinished: (values) {
onFinished(values.entries.first.value);
},
onNext: (page, values) {},
),
),
);
}
}

View file

@ -0,0 +1,146 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// The shopping cart screen.
class ShoppingCartScreen extends StatelessWidget {
/// Constructor for the shopping cart screen.
const ShoppingCartScreen({
required this.shoppingService,
required this.translations,
required this.onOrder,
required this.options,
super.key,
});
/// The shopping service.
final ShoppingService shoppingService;
/// The translations.
final ShoppingTranslations translations;
/// the on order callback.
final Function() onOrder;
/// The options.
final FlutterShoppingOptions options;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: Text(translations.shoppingCartTitle),
),
body: StreamBuilder(
stream: shoppingService.getShoppingCart(),
builder: (context, snapshot) {
if (snapshot.hasData) {
var cart = snapshot.data!;
return Stack(
children: [
SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
translations.shoppingCartProducts,
style: theme.textTheme.titleLarge,
),
Column(
children: cart.products
.map(
(product) =>
options.shoppingCartItemBuilder?.call(
context,
product,
() async {
await shoppingService
.addProductToCart(product);
},
() async {
await shoppingService
.removeProductFromCart(product);
},
) ??
ShoppingCartItem(
options: options,
translations: translations,
product: product,
onAddToCart: (product) async {
await shoppingService
.addProductToCart(product);
},
onRemoveFromCart: (product) async {
await shoppingService
.removeProductFromCart(product);
},
),
)
.toList(),
),
const SizedBox(
height: 200,
),
],
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: ColoredBox(
color: theme.scaffoldBackgroundColor,
child: Padding(
padding: const EdgeInsets.only(
bottom: 32,
left: 64,
right: 64,
top: 16,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
translations.shoppingCartTotal,
style: theme.textTheme.titleMedium,
),
Text(
"${translations.shoppingCartCurrency}"
// ignore: lines_longer_than_80_chars
" ${cart.totalAmountWithDiscount.toStringAsFixed(2)}",
style: theme.textTheme.titleMedium,
),
],
),
const SizedBox(
height: 24,
),
options.primaryButtonBuilder?.call(
context,
cart.products.isNotEmpty,
translations.orderButton,
onOrder,
) ??
PrimaryButton(
enabled: cart.products.isNotEmpty,
text: translations.orderButton,
onPressed: onOrder,
),
],
),
),
),
),
],
);
}
return const SizedBox.shrink();
},
),
);
}
}

View file

@ -0,0 +1,252 @@
import "package:collection/collection.dart";
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// The shopping screen.
class ShoppingScreen extends StatefulWidget {
/// Constructor for the shopping screen.
const ShoppingScreen({
required this.shoppingService,
required this.translations,
required this.onShoppingCartPressed,
required this.options,
this.onFilterPressed,
this.initialShopId,
super.key,
});
/// The shopping service.
final ShoppingService shoppingService;
/// The translations.
final ShoppingTranslations translations;
/// The initial shop id.
final String? initialShopId;
/// The on filter pressed function.
final Function()? onFilterPressed;
/// The on shopping cart pressed function.
final Function() onShoppingCartPressed;
/// The options for shopping.
final FlutterShoppingOptions options;
@override
State<ShoppingScreen> createState() => _ShoppingScreenState();
}
class _ShoppingScreenState extends State<ShoppingScreen> {
Shop? selectedShop;
List<Category> selectedCategories = [];
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var service = widget.shoppingService;
var options = widget.options;
return Scaffold(
drawer: options.shoppingScreenDrawer,
appBar: options.shoppingScreenAppbarBuilder?.call(
context,
selectedShop?.adress ?? "",
widget.onFilterPressed,
) ??
AppBar(
title: Text(
selectedShop?.adress ?? "",
style: theme.textTheme.headlineLarge,
),
leading: IconButton(
onPressed: () {},
icon: const Icon(
Icons.person_rounded,
size: 28,
),
),
actions: [
IconButton(
onPressed: widget.onFilterPressed,
icon: const Icon(
Icons.filter_alt_rounded,
size: 28,
),
),
],
),
body: Stack(
children: [
StreamBuilder(
stream: widget.shoppingService.getShops(),
builder: (context, snapshot) {
if (snapshot.hasData) {
selectedShop ??= widget.shoppingService.selectShop(
widget.initialShopId ?? snapshot.data!.first.id,
);
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ShopSelector(
shops: snapshot.data!,
options: options,
shoppingService: widget.shoppingService,
onSelected: (shop) {
service.selectShop(shop.id);
setState(() {
selectedShop = shop;
});
},
),
),
StreamBuilder(
stream: service.getSelectedCategoryStream(),
builder: (context, snapshot) {
if (snapshot.hasData) {
selectedCategories = snapshot.data!;
return Padding(
padding:
const EdgeInsets.symmetric(horizontal: 20),
child: Wrap(
spacing: 8,
children: snapshot.data!
.map(
(category) =>
options.filterChipbuilder?.call(
context, category.name, () {
service
.deselectCategory(category.id);
}) ??
FilterChip(
backgroundColor: theme.primaryColor,
onSelected: (value) {},
label: Text(
category.name,
style: theme.textTheme.bodyMedium
?.copyWith(
color: Colors.white,
),
),
deleteIcon: const Icon(
Icons.close_rounded,
color: Colors.white,
),
onDeleted: () {
service.deselectCategory(
category.id,
);
},
),
)
.toList(),
),
);
} else {
return const SizedBox.shrink();
}
},
),
StreamBuilder(
stream: widget.shoppingService
.getProducts(selectedShop!.id),
builder: (context, snapshot) {
if (snapshot.hasData) {
var products = groupBy(
snapshot.data!,
(Product p) => p.category,
);
var weeklyOffer = service.getWeeklyOffer();
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 32,
vertical: 20,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (weeklyOffer != null) ...[
options.weeklyOfferBuilder?.call(
context,
weeklyOffer,
) ??
WeeklyOffer(
product: weeklyOffer,
translations: widget.translations,
),
],
Row(
children: [
Text(
widget.translations
.whatWouldyouLikeToOrder,
style: theme.textTheme.titleLarge,
textAlign: TextAlign.start,
),
],
),
Padding(
padding: const EdgeInsets.only(
top: 20,
bottom: 100,
),
child: ProductItemList(
options: widget.options,
translations: widget.translations,
products: products,
onAddToCart: (product) async {
await service.addProductToCart(product);
},
),
),
],
),
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
],
),
);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
StreamBuilder(
stream: service.getCartLength(),
builder: (context, snapshot) {
var cartLength = snapshot.data ?? 0;
return Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 32),
child: options.primaryButtonBuilder?.call(
context,
cartLength > 0,
widget.translations.viewShoppingCart, () {
widget.onShoppingCartPressed();
}) ??
PrimaryButton(
enabled: cartLength > 0,
text: widget.translations.viewShoppingCart,
onPressed: widget.onShoppingCartPressed,
),
),
);
},
),
],
),
);
}
}

View file

@ -0,0 +1,44 @@
// ignore_for_file: avoid_positional_boolean_parameters
import "package:flutter/material.dart";
import "package:shopping_repository_interface/shopping_repository_interface.dart";
/// A filter item.
class FilterItem extends StatelessWidget {
/// Constructor for the filter item.
const FilterItem({
required this.value,
required this.onChanged,
required this.category,
super.key,
});
/// The category.
final Category category;
/// The value.
final bool value;
/// The on changed function.
final Function(bool value) onChanged;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: CheckboxListTile(
controlAffinity: ListTileControlAffinity.leading,
value: value,
onChanged: (value) {
onChanged(value ?? false);
},
shape: const UnderlineInputBorder(),
title: Text(
category.name,
style: theme.textTheme.bodyMedium,
),
),
);
}
}

View file

@ -0,0 +1,45 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// A bottomsheet that shows information.
class InfoBottomsheet extends StatelessWidget {
/// Constructor for the InfoBottomsheet.
const InfoBottomsheet({
required this.productInfo,
required this.translations,
super.key,
});
/// The product info.
final String productInfo;
/// The translations.
final ShoppingTranslations translations;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
child: Text(
productInfo,
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
),
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: PrimaryButton(
text: translations.closeInfo,
onPressed: () {
Navigator.of(context).pop();
},
),
),
],
);
}
}

View file

@ -0,0 +1,41 @@
import "package:flutter/material.dart";
/// A primary button.
class PrimaryButton extends StatelessWidget {
/// Constructor for the primary button.
const PrimaryButton({
required this.text,
required this.onPressed,
this.enabled = true,
super.key,
});
/// The text.
final String text;
/// The on pressed function.
final Function() onPressed;
/// whether the button is enabled.
final bool enabled;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 254,
),
child: FilledButton(
onPressed: enabled ? onPressed : null,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
text,
style: theme.textTheme.displayLarge,
),
),
),
);
}
}

View file

@ -0,0 +1,139 @@
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// Shop item.
class ProductItem extends StatelessWidget {
/// Constructor for the shop item.
const ProductItem({
required this.product,
required this.onAddToCart,
required this.translations,
required this.options,
super.key,
});
/// The product.
final Product product;
/// The on add to cart function.
final Function(Product product) onAddToCart;
/// The translations.
final ShoppingTranslations translations;
/// The options.
final FlutterShoppingOptions options;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.only(top: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: CachedNetworkImageProvider(
product.imageUrl,
),
fit: BoxFit.cover,
),
),
),
const SizedBox(
width: 16,
),
Text(
product.name,
style: theme.textTheme.titleMedium,
),
IconButton(
onPressed: () {
options.onInfoPressed?.call(product) ??
showBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
builder: (context) => InfoBottomsheet(
productInfo: product.description,
translations: translations,
),
);
},
icon: Icon(
Icons.info_outline_rounded,
color: theme.primaryColor,
),
),
],
),
Row(
children: [
Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
if (product.isDiscounted) ...[
Text(
product.price.toStringAsFixed(2),
style: theme.textTheme.labelSmall?.copyWith(
decoration: TextDecoration.lineThrough,
),
),
Container(
height: 30,
),
],
],
),
const SizedBox(
width: 8,
),
Column(
children: [
Text(
product.isDiscounted
? product.discountPrice.toStringAsFixed(2)
: product.price.toStringAsFixed(2),
style: theme.textTheme.bodySmall?.copyWith(fontSize: 14),
),
InkWell(
onTap: () async {
await onAddToCart(product);
},
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: theme.primaryColor,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
"+",
style: theme.textTheme.titleMedium?.copyWith(
color: Colors.white,
),
),
),
),
),
],
),
],
),
],
),
);
}
}

View file

@ -0,0 +1,72 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// ProductItemList
class ProductItemList extends StatelessWidget {
/// Constructor for the product item list.
const ProductItemList({
required this.products,
required this.onAddToCart,
required this.translations,
required this.options,
super.key,
});
/// The items.
final Map<String, List<Product>> products;
/// The on add to cart function.
final Function(Product) onAddToCart;
/// The translations.
final ShoppingTranslations translations;
/// The options.
final FlutterShoppingOptions options;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Column(
children: products.entries
.map(
(entry) => Column(
children: [
Row(
children: [
Text(
entry.key,
style: theme.textTheme.titleMedium,
textAlign: TextAlign.start,
),
],
),
Column(
children: entry.value
.map(
(product) =>
options.productItemBuilder?.call(
context,
product,
) ??
ProductItem(
options: options,
product: product,
translations: translations,
onAddToCart: (product) async {
await onAddToCart(product);
},
),
)
.toList(),
),
const SizedBox(
height: 20,
),
],
),
)
.toList(),
);
}
}

View file

@ -0,0 +1,53 @@
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// Shop selector.
class ShopSelector extends StatelessWidget {
/// Constructor for the shop selector.
const ShopSelector({
required this.shoppingService,
required this.onSelected,
required this.options,
required this.shops,
super.key,
});
/// The shopping service.
final ShoppingService shoppingService;
/// The on selected function.
final Function(Shop) onSelected;
/// The options.
final FlutterShoppingOptions options;
///
final List<Shop> shops;
@override
Widget build(BuildContext context) => SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
Row(
children: shops
.map(
(shop) => Row(
children: [
const SizedBox(width: 4),
options.shopSelectorItemBuilder
?.call(context, shop, onSelected) ??
ShopSelectorItem(
shoppingService: shoppingService,
onSelected: onSelected,
shop: shop,
),
],
),
)
.toList(),
),
],
),
);
}

View file

@ -0,0 +1,48 @@
import "package:flutter/material.dart";
import "package:shopping_repository_interface/shopping_repository_interface.dart";
/// Shop selector item.
class ShopSelectorItem extends StatelessWidget {
/// Constructor for the shop selector item.
const ShopSelectorItem({
required this.shop,
required this.onSelected,
required this.shoppingService,
super.key,
});
/// The shop.
final Shop shop;
/// The on selected function.
final Function(Shop) onSelected;
/// The shopping service.
final ShoppingService shoppingService;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
var isSelected = shop.id == shoppingService.getSelectedShop()?.id;
return InkWell(
onTap: () => onSelected(shop),
child: DecoratedBox(
decoration: BoxDecoration(
border: !isSelected ? Border.all(color: theme.primaryColor) : null,
color: isSelected ? theme.primaryColor : Colors.white,
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
shop.name,
style: isSelected
? theme.textTheme.titleMedium?.copyWith(color: Colors.white)
: theme.textTheme.bodyMedium,
),
),
),
);
}
}

View file

@ -0,0 +1,149 @@
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// The shopping cart item.
class ShoppingCartItem extends StatelessWidget {
/// Constructor for the shopping cart item.
const ShoppingCartItem({
required this.product,
required this.onAddToCart,
required this.onRemoveFromCart,
required this.translations,
required this.options,
super.key,
});
/// The product.
final Product product;
/// The on add to cart function.
final Function(Product product) onAddToCart;
/// The on remove from cart function.
final Function(Product product) onRemoveFromCart;
/// The translations.
final ShoppingTranslations translations;
/// The options.
final FlutterShoppingOptions options;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.only(top: 20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: DecorationImage(
image: CachedNetworkImageProvider(
product.imageUrl,
),
fit: BoxFit.cover,
),
),
),
const SizedBox(
width: 16,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
product.name,
style: theme.textTheme.titleMedium,
),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () {
options.onInfoPressed?.call(product) ??
showBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
builder: (context) => InfoBottomsheet(
productInfo: product.description,
translations: translations,
),
);
},
icon: Icon(
Icons.info_outline_rounded,
color: theme.primaryColor,
),
),
],
),
],
),
Column(
children: [
Text(
product.isDiscounted
? product.discountPrice.toStringAsFixed(2)
: product.price.toStringAsFixed(2),
style: theme.textTheme.bodySmall?.copyWith(fontSize: 14),
),
Row(
children: [
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () async {
await onRemoveFromCart(product);
},
icon: const Icon(Icons.remove_rounded),
),
InkWell(
onTap: () async {
await onAddToCart(product);
},
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: theme.primaryColor,
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
product.selectedAmount.toString(),
style: theme.textTheme.titleMedium?.copyWith(
color: Colors.white,
),
),
),
),
),
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
onPressed: () async {
await onAddToCart(product);
},
icon: const Icon(Icons.add_rounded),
),
],
),
],
),
],
),
);
}
}

View file

@ -0,0 +1,85 @@
import "package:cached_network_image/cached_network_image.dart";
import "package:flutter/material.dart";
import "package:flutter_shopping/flutter_shopping.dart";
/// Weekly offer.
class WeeklyOffer extends StatelessWidget {
/// Constructor for the weekly offer.
const WeeklyOffer({
required this.product,
required this.translations,
super.key,
});
/// The product.
final Product? product;
/// The translations.
final ShoppingTranslations translations;
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.only(bottom: 24),
child: Container(
width: double.infinity,
height: 350,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.black,
width: 1,
),
image: DecorationImage(
image: CachedNetworkImageProvider(
product!.imageUrl,
),
fit: BoxFit.fitWidth,
),
),
child: Column(
children: [
Container(
decoration: const BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
),
width: double.infinity,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
translations.weeklyOffer,
style: theme.textTheme.headlineSmall,
),
),
),
const Spacer(),
Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(8),
bottomRight: Radius.circular(8),
),
),
width: double.infinity,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(
"${product!.name}, now for ${product!.discountPrice} each",
style: theme.textTheme.bodyMedium,
),
),
),
],
),
),
);
}
}

View file

@ -1,47 +1,37 @@
name: flutter_shopping
description: "A new Flutter project."
publish_to: "none"
version: 2.0.0
description: "A new Flutter package project."
version: 3.0.0
homepage: https://github.com/Iconica-Development/flutter_shopping/packages/flutter_shopping
publish_to: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
environment:
sdk: ">=3.3.4 <4.0.0"
sdk: ^3.5.1
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
go_router: any
flutter_product_page:
git:
url: https://github.com/Iconica-Development/flutter_shopping
ref: 2.0.0
path: packages/flutter_product_page
flutter_shopping_cart:
git:
url: https://github.com/Iconica-Development/flutter_shopping
ref: 2.0.0
path: packages/flutter_shopping_cart
flutter_order_details:
git:
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
cached_network_image: ^3.4.1
rxdart: ^0.28.0
collection: ^1.18.0
animated_toggle:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
version: ^0.0.3
flutter_form_wizard:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
version: ^6.6.0
shopping_repository_interface:
hosted: https://forgejo.internal.iconica.nl/api/packages/internal/pub/
version: ^3.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:
assets:
- assets/

View file

@ -1,77 +0,0 @@
# flutter_shopping_cart
This component contains a shopping cart screen and the functionality for shopping carts that contain products.
## Features
* Shopping cart screen
* Shopping cart products
## Usage
First, create your own product by extending the `Product` class:
```dart
class ExampleProduct extends Product {
ExampleProduct({
requried super.id,
required super.name,
required super.price,
required this.image,
super.quantity,
});
final String image;
}
```
Next, you can create the `ShoppingCartScreen` widget like this:
```dart
var myProductService = ProductService<ExampleProduct>([]);
ShoppingCartScreen<ExampleProduct>(
configuration: ShoppingCartConfig<ExampleProduct>(
productService: myProductService,
//
productItemBuilder: (
BuildContext context,
Locale locale,
ExampleProduct product,
) =>
ListTile(
title: Text(product.name),
subtitle: Text(product.price.toString()),
),
//
onConfirmOrder: (List<ExampleProduct> products) {
print("Placing order with products: $products");
},
),
);
```
For a more detailed example you can see the [example](https://github.com/Iconica-Development/flutter_shopping_cart/tree/main/example).
Or, you could run the example yourself:
```
git clone https://github.com/Iconica-Development/flutter_shopping_cart.git
cd flutter_shopping_cart
cd example
flutter run
```
## Issues
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_shopping_cart) page. Commercial support is available if you need help with integration with your app or services. You can contact us at [support@iconica.nl](mailto:support@iconica.nl).
## Want to contribute
If you would like to contribute to the component (e.g. by improving the documentation, solving a bug or adding a cool new feature), please carefully review our [contribution guide](./CONTRIBUTING.md) and send us your [pull request](https://github.com/Iconica-Development/flutter_shopping_cart/pulls).
## Author
This flutter_shopping_cart for Flutter is developed by [Iconica](https://iconica.nl). You can contact us at <support@iconica.nl>

View file

@ -1,9 +0,0 @@
include: package:flutter_iconica_analysis/components_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -1,6 +0,0 @@
/// Flutter component for shopping cart.
library flutter_shopping_cart;
export "src/config/shopping_cart_config.dart";
export "src/config/shopping_cart_translations.dart";
export "src/widgets/shopping_cart_screen.dart";

View file

@ -1,89 +0,0 @@
import "package:flutter/material.dart";
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 {
/// Creates a shopping cart configuration.
ShoppingCartConfig({
required this.service,
required this.onConfirmOrder,
this.productItemBuilder,
this.confirmOrderButtonBuilder,
this.confirmOrderButtonHeight = 100,
this.sumBottomSheetBuilder,
this.sumBottomSheetHeight = 100,
this.titleBuilder,
this.translations = const ShoppingCartTranslations(),
this.pagePadding = const EdgeInsets.symmetric(horizontal: 32),
this.bottomPadding = const EdgeInsets.fromLTRB(44, 0, 44, 32),
this.appBarBuilder,
});
/// Product service. The product service is used to manage the products in the
/// shopping cart.
final ShoppingCartService service;
/// Product item builder. This builder is used to build the product item
/// that will be displayed in the shopping cart.
Widget Function(
BuildContext context,
Product product,
ShoppingCartConfig configuration,
)? productItemBuilder;
/// Confirm order button builder. This builder is used to build the confirm
/// order button that will be displayed in the shopping cart.
/// If you override this builder, you cannot use the [onConfirmOrder] callback
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.
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<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.
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.
double sumBottomSheetHeight;
/// Padding around the shopping cart. The padding is used to create space
/// around the shopping cart.
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.
EdgeInsets bottomPadding;
/// Title builder. This builder is used to
/// build the title of the shopping cart.
final Widget Function(
BuildContext context,
String title,
)? titleBuilder;
/// Shopping cart translations. The translations for the shopping cart.
ShoppingCartTranslations translations;
/// Appbar for the shopping cart screen.
PreferredSizeWidget Function(BuildContext context)? appBarBuilder;
}

View file

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

View file

@ -1,23 +0,0 @@
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

@ -1,51 +0,0 @@
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

@ -1,134 +0,0 @@
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

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

View file

@ -1,131 +0,0 @@
import "package:flutter/material.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 extends StatefulWidget {
/// Creates a shopping cart screen.
const ShoppingCartScreen({
required this.configuration,
super.key,
});
/// Configuration for the shopping cart screen.
final ShoppingCartConfig configuration;
@override
State<ShoppingCartScreen> createState() => _ShoppingCartScreenState();
}
class _ShoppingCartScreenState extends State<ShoppingCartScreen> {
@override
Widget build(BuildContext context) {
var theme = Theme.of(context);
return Scaffold(
appBar: widget.configuration.appBarBuilder?.call(context) ??
const DefaultAppbar(),
body: SafeArea(
child: Stack(
fit: StackFit.expand,
children: [
Padding(
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(
configuration: widget.configuration,
),
),
],
),
),
);
}
}
class _BottomSheet extends StatelessWidget {
const _BottomSheet({
required this.configuration,
});
final ShoppingCartConfig configuration;
@override
Widget build(BuildContext context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
configuration.sumBottomSheetBuilder?.call(context, configuration) ??
DefaultSumBottomSheetBuilder(
configuration: configuration,
),
configuration.confirmOrderButtonBuilder?.call(
context,
configuration,
configuration.onConfirmOrder,
) ??
DefaultConfirmOrderButton(
configuration: configuration,
onConfirmOrder: configuration.onConfirmOrder,
),
],
);
}

View file

@ -1,27 +0,0 @@
name: flutter_shopping_cart
description: "A Flutter module for a shopping cart."
version: 2.0.0
publish_to: "none"
environment:
sdk: ">=3.3.0 <4.0.0"
flutter: ">=1.17.0"
dependencies:
flutter:
sdk: flutter
flutter_shopping_interface:
git:
url: https://github.com/Iconica-Development/flutter_shopping
path: packages/flutter_shopping_interface
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:

View file

@ -1,9 +0,0 @@
include: package:flutter_iconica_analysis/components_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -1,2 +0,0 @@
export "src/model/model.dart";
export "src/service/service.dart";

View file

@ -1,2 +0,0 @@
export "product.dart";
export "shop.dart";

View file

@ -1,85 +0,0 @@
/// Product Interface
abstract class ProductInterface {
/// ProductInterface constructor
ProductInterface({
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,
});
/// Product id
final String id;
/// Product name
final String name;
/// Product image url
final String imageUrl;
/// Product category
final String category;
/// Product price
final double price;
/// whether the product has a discount
final bool hasDiscount;
/// Product discount price
final double? discountPrice;
/// Product quantity
int quantity;
/// Product description
final String description;
}
/// Product model
class Product implements ProductInterface {
/// Product constructor
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,
});
@override
final String id;
@override
final String name;
@override
final String imageUrl;
@override
final String category;
@override
final double price;
@override
final bool hasDiscount;
@override
final double? discountPrice;
@override
int quantity;
@override
final String description;
}

View file

@ -1,28 +0,0 @@
/// Shop interface
abstract class ShopInterface {
/// ShopInterface constructor
const ShopInterface({
required this.id,
required this.name,
});
/// Shop id
final String id;
/// Shop name
final String name;
}
/// Shop model
class Shop implements ShopInterface {
/// Shop constructor
const Shop({
required this.id,
required this.name,
});
@override
final String id;
@override
final String name;
}

View file

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

View file

@ -1,23 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_shopping_interface/src/model/product.dart";
/// Product service
abstract class ProductService with ChangeNotifier {
/// Retrieve a list of products
Future<List<Product>> getProducts(String shopId);
/// Retrieve a product
Future<Product> getProduct(String id);
/// 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

@ -1,5 +0,0 @@
export "order_service.dart";
export "product_service.dart";
export "shop_service.dart";
export "shopping_cart_service.dart";
export "shopping_service.dart";

View file

@ -1,15 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_shopping_interface/src/model/shop.dart";
/// Shop service
// ignore: one_member_abstracts
abstract class ShopService with ChangeNotifier {
/// Retrieve a list of shops
Future<List<Shop>> getShops();
/// Select a shop
void selectShop(Shop shop);
/// The currently selected shop
Shop? get selectedShop;
}

View file

@ -1,23 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_shopping_interface/src/model/product.dart";
/// shopping cart service
abstract class ShoppingCartService with ChangeNotifier {
/// Adds a product to the shopping cart.
void addProduct(Product product);
/// Removes a product from the shopping cart.
void removeProduct(Product product);
/// Removes one product from the shopping cart.
void removeOneProduct(Product product);
/// Counts the number of products in the shopping cart.
int countProducts();
/// Clears the shopping cart.
void clear();
/// The list of products in the shopping cart.
List<Product> get products;
}

View file

@ -1,24 +0,0 @@
import "package:flutter_shopping_interface/src/service/service.dart";
/// Shopping service
class ShoppingService {
/// Shopping service constructor
const ShoppingService({
required this.orderService,
required this.productService,
required this.shopService,
required this.shoppingCartService,
});
/// Order service
final OrderService orderService;
/// Product service
final ProductService productService;
/// Shop service
final ShopService shopService;
/// Shopping cart service
final ShoppingCartService shoppingCartService;
}

View file

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

View file

@ -1,9 +0,0 @@
include: package:flutter_iconica_analysis/components_options.yaml
# Possible to overwrite the rules from the package
analyzer:
exclude:
linter:
rules:

View file

@ -1 +0,0 @@
export "service/service.dart";

View file

@ -1,15 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Local order service
class LocalOrderService with ChangeNotifier implements OrderService {
@override
Future<void> createOrder(
String shopId,
List<Product> products,
Map<int, Map<String, dynamic>> clientInformation,
) async {
// Create the order
notifyListeners();
}
}

View file

@ -1,103 +0,0 @@
import "package:flutter/material.dart";
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() =>
_allProducts.map((e) => e.category).toSet().toList();
@override
Future<Product> getProduct(String id) =>
Future.value(_products.firstWhere((element) => element.id == id));
@override
Future<List<Product>> getProducts(String shopId) async {
await Future.delayed(const Duration(seconds: 1));
_products = [
Product(
id: "1",
name: "White Bread",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/appshell-demo"
".appspot.com/o/shopping%2Fwhite.png"
"?alt=media&token=e3aa13d5-932a-4119-bdbb-89c1a1d82213",
category: "Bread",
price: 1.0,
description: "This is a delicious white bread",
hasDiscount: true,
discountPrice: 0.5,
),
Product(
id: "2",
name: "Brown Bread",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/appshell-"
"demo.appspot.com/o/shopping%2Fbrown.png?alt=media&"
"token=fbfe280d-44e6-4cde-a491-bcb7f598d22c",
category: "Bread",
price: 2.0,
description: "This is a delicious brown bread",
),
Product(
id: "3",
name: "White fish",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/appshell"
"-demo.appspot.com/o/shopping%2Fwhite-fish.png?alt=media"
"&token=61c44f84-347d-4b42-a10d-c172f167929b",
category: "Fish",
price: 1.5,
description: "This is a delicious white fish",
),
Product(
id: "4",
name: "Brown fish",
imageUrl: "https://firebasestorage.googleapis.com/v0/b/appshel"
"l-demo.appspot.com/o/shopping%2Fbrown-fish.png?alt=media&"
"token=b743a53c-c4bb-49ac-8894-a08132992902",
category: "Fish",
price: 1.5,
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

@ -1,35 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// A service that provides a list of shops.
class LocalShopService with ChangeNotifier implements ShopService {
Shop? _selectedShop;
@override
Future<List<Shop>> getShops() async {
await Future.delayed(const Duration(seconds: 1));
var shops = <Shop>[
const Shop(id: "1", name: "Bakkerij de Goudkorst"),
const Shop(id: "2", name: "Slagerij PuurVlees"),
const Shop(id: "3", name: "De GroenteHut"),
const Shop(id: "4", name: "Pizzeria Ciao"),
const Shop(id: "5", name: "Cafetaria Roos"),
const Shop(id: "6", name: "Zeebries Visdelicatessen"),
const Shop(id: "7", name: "De Oosterse Draak"),
];
return Future.value(shops);
}
/// Updates the selected shop.
@override
void selectShop(Shop shop) {
if (_selectedShop == shop) return;
_selectedShop = shop;
notifyListeners();
}
/// The currently selected shop.
@override
Shop? get selectedShop => _selectedShop;
}

View file

@ -1,60 +0,0 @@
import "package:flutter/material.dart";
import "package:flutter_shopping_interface/flutter_shopping_interface.dart";
/// Local shopping cart service
class LocalShoppingCartService
with ChangeNotifier
implements ShoppingCartService {
final List<Product> _products = [];
@override
void addProduct(Product product) {
if (_products.contains(product)) {
var index = _products.indexOf(product);
_products[index].quantity++;
} else {
_products.add(product);
}
notifyListeners();
}
@override
void clear() {
_products.clear();
notifyListeners();
}
@override
int countProducts() {
var count = 0;
for (var product in _products) {
count += product.quantity;
}
notifyListeners();
return count;
}
@override
void removeOneProduct(Product product) {
if (_products.contains(product)) {
var index = _products.indexOf(product);
if (_products[index].quantity > 1) {
_products[index].quantity--;
} else {
_products.removeAt(index);
}
}
notifyListeners();
}
@override
void removeProduct(Product product) {
if (_products.contains(product)) {
var index = _products.indexOf(product);
_products.removeAt(index);
}
notifyListeners();
}
@override
List<Product> get products => _products;
}

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