This commit is contained in:
mike doornenbal 2024-10-22 09:16:15 +00:00 committed by GitHub
commit b62e958d44
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
132 changed files with 3649 additions and 4574 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

82
README.md Normal file
View file

@ -0,0 +1,82 @@
# Flutter Shopping
Flutter Shopping is a package that allows you to create a shopping experience in your Flutter app.
By default, it uses a `ShoppingService` with a local implementation ,it provides a simple shopping
experience with a list of products and a shopping cart. You can implement your own `ShoppingService` by overriding the `shopRepository`, `cartRepository`, `categoryRepository` and `orderRepository` properties.
## Setup
To use this package, add flutter_shopping as a dependency in your pubspec.yaml file:
```
flutter_chat:
git:
url: https://github.com/Iconica-Development/flutter_shopping
path: packages/flutter_shopping
```
If you are going to use Firebase as the back-end of flutter_shopping, you should also add the following package as a dependency to your pubspec.yaml file:
```
firebase_shopping_repository:
git:
url: https://github.com/Iconica-Development/flutter_shopping
path: packages/firebase_shopping_repository
```
Create a Firebase project for your application and add firebase firestore and storage.
make sure you are authenticated using the `Firebase_auth` package or adjust your firebase rules, otherwise you won't be able to retreive data.
Also make sure you have the corresponding collections in your firebase project as defined in the `FirebaseRepositories`, you can override the
default paths as you wish:
``` dart
FirebaseCategoryRepository({
this.collectionName = 'shopping_category',
});
FirebaseCartRepository({
this.collectionName = 'shopping_cart',
});
FirebaseOrderRepository({
this.collectionName = 'shopping_order',
});
```
Also the structure of your data should be equal to our predefined models.
## How to use
To use this package, add the following widget `FlutterShoppingNavigatorUserstory` to your widget tree:
``` dart
class FlutterShopping extends StatelessWidget {
const FlutterShopping({super.key});
@override
Widget build(BuildContext context) {
return FlutterShoppingNavigatorUserstory(
options: const FlutterShoppingOptions(),
translations: const ShoppingTranslations(),
shoppingService: ShoppingService(),
initialShopId: "1",
);
}
}
```
All of the properties are optional.
## Issues
Please file any issues, bugs or feature request as an issue on our [GitHub](https://github.com/Iconica-Development/flutter_shopping/pulls) 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 plugin (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>

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 @@
../../CHANGELOG.md

View file

@ -0,0 +1 @@
../../CONTRIBUTING.md

View file

@ -0,0 +1 @@
../../LICENSE

View file

@ -0,0 +1 @@
../../README.md

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,57 @@
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 {
FirebaseCategoryRepository({
this.collectionName = 'shopping_category',
});
final String collectionName;
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(collectionName)
.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,62 @@
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 {
FirebaseProductRepository({
this.collectionName = 'shopping_products',
});
final String collectionName;
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(collectionName)
.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,50 @@
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 {
FirebaseShopRepository({
this.collectionName = 'shopping_shop',
});
final String collectionName;
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(collectionName)
.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,77 @@
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

@ -0,0 +1 @@
../../CHANGELOG.md

View file

@ -0,0 +1 @@
../../CONTRIBUTING.md

View file

@ -0,0 +1 @@
../../LICENSE

View file

@ -1,181 +0,0 @@
# flutter_shopping
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.
## Setup
(1) Set up your `MyShop` model by extending from the `ProductPageShop` class. The most basic version looks like this:
```dart
class MyShop extends ProductPageShop {
const MyShop({
required super.id,
required super.name,
});
}
```
(2) Set up your `MyProduct` model by extending from `ShoppingCartProduct` and extending from the mixin `ProductPageProduct`, like this:
```dart
class MyProduct extends ShoppingCartProduct with ProductPageProduct {
MyProduct({
required super.id,
required super.name,
required super.price,
required this.category,
required this.imageUrl,
this.discountPrice,
this.hasDiscount = false,
});
@override
final String category;
@override
final String imageUrl;
@override
final double? discountPrice;
@override
final bool hasDiscount;
}
```
(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)
## Usage
For a detailed example you can see the [example](https://github.com/Iconica-Development/flutter_shopping/tree/main/example).
Or, you could run the example yourself:
```
git clone https://github.com/Iconica-Development/flutter_shopping.git
cd flutter_shopping
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) 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>

View file

@ -0,0 +1 @@
../../README.md

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,33 @@
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 FlutterShopping(),
);
}
}
class FlutterShopping extends StatelessWidget {
const FlutterShopping({super.key});
@override
Widget build(BuildContext context) {
return FlutterShoppingNavigatorUserstory(
options: const FlutterShoppingOptions(),
translations: const ShoppingTranslations(),
shoppingService: ShoppingService(),
initialShopId: "1",
);
}
}

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();
}
@override
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());
},
getProducts: (shop) async {
if (shoppingConfiguration.onGetProducts != null) {
return shoppingConfiguration.onGetProducts!(shop.id);
} else {
return service.productService.getProducts(shop.id);
}
onShoppingCartPressed: () async {
widget.options.onCartPressed?.call() ??
await push(shoppingCartScreen());
},
onAddToCart: (product) {
if (shoppingConfiguration.onAddToCart != null) {
shoppingConfiguration.onAddToCart!(product);
return;
} else {
return service.shoppingCartService.addProduct(product);
}
);
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());
},
onNavigateToShoppingCart: () async {
if (shoppingConfiguration.onNavigateToShoppingCart != null) {
return shoppingConfiguration.onNavigateToShoppingCart!();
} else {
return Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ShoppingCart(
shoppingConfiguration: shoppingConfiguration,
),
),
);
}
);
Widget personalInformationScreen() => PersonalInformationScreen(
translations: widget.translations,
options: widget.options,
onFinished: (value) async {
widget.options.getPersonalInformation?.call(value);
widget.options.onPersonalInformationPressed?.call() ??
await push(dateTimeInformationScreen());
},
getProductsInShoppingCart: () {
if (shoppingConfiguration.getProductsInShoppingCart != null) {
return shoppingConfiguration.getProductsInShoppingCart!();
} else {
return service.shoppingCartService.countProducts();
}
);
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 cart.
class ShoppingCart extends StatelessWidget {
/// Constructor for the shopping cart.
const ShoppingCart({
required this.shoppingConfiguration,
super.key,
});
/// Shopping configuration.
final ShoppingConfiguration shoppingConfiguration;
@override
Widget build(BuildContext context) {
var service = shoppingConfiguration.shoppingService.shoppingCartService;
return ShoppingCartScreen(
configuration: ShoppingCartConfig(
service: service,
productItemBuilder: shoppingConfiguration.productItemBuilder,
confirmOrderButtonBuilder:
shoppingConfiguration.confirmOrderButtonBuilder,
confirmOrderButtonHeight:
shoppingConfiguration.confirmOrderButtonHeight ?? 100,
sumBottomSheetBuilder: shoppingConfiguration.sumBottomSheetBuilder,
sumBottomSheetHeight: shoppingConfiguration.sumBottomSheetHeight ?? 100,
titleBuilder: shoppingConfiguration.titleBuilder,
translations: shoppingConfiguration.shoppingCartTranslations ??
const ShoppingCartTranslations(),
pagePadding: shoppingConfiguration.shoppingCartPagePadding ??
const EdgeInsets.symmetric(horizontal: 32),
bottomPadding: shoppingConfiguration.shoppingCartBottomPadding ??
const EdgeInsets.fromLTRB(44, 0, 44, 32),
appBarBuilder: shoppingConfiguration.shoppingCartAppBarBuilder,
onConfirmOrder: (products) async {
if (shoppingConfiguration.onConfirmOrder != null) {
return shoppingConfiguration.onConfirmOrder!(products);
} else {
return Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ShoppingOrderDetails(
shoppingConfiguration: shoppingConfiguration,
),
),
);
}
},
),
);
}
}
/// Shopping order details.
class ShoppingOrderDetails extends StatelessWidget {
/// Constructor for the shopping order details.
const ShoppingOrderDetails({
required this.shoppingConfiguration,
super.key,
});
/// Shopping configuration.
final ShoppingConfiguration shoppingConfiguration;
@override
Widget build(BuildContext context) => OrderDetailScreen(
configuration: OrderDetailConfiguration(
shoppingService: shoppingConfiguration.shoppingService,
pages: shoppingConfiguration.pages,
translations: shoppingConfiguration.orderDetailTranslations ??
const OrderDetailTranslations(),
appBarBuilder: shoppingConfiguration.orderDetailAppBarBuilder,
nextbuttonBuilder: shoppingConfiguration.orderDetailNextbuttonBuilder,
orderSuccessBuilder: (context, configuration, data) =>
shoppingConfiguration.orderSuccessBuilder
?.call(context, configuration, data) ??
DefaultOrderSucces(
configuration: configuration,
orderDetails: data,
),
onNextStep: (currentStep, data, controller) async {
if (shoppingConfiguration.onNextStep != null) {
return shoppingConfiguration.onNextStep!(
currentStep,
data,
controller,
);
} else {
await controller.autoNextStep();
}
},
onStepsCompleted: (shopId, products, data, configuration) async {
if (shoppingConfiguration.onStepsCompleted != null) {
return shoppingConfiguration.onStepsCompleted!(
shopId,
products,
data,
configuration,
);
} else {
return Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => DefaultOrderSucces(
configuration: configuration,
orderDetails: data,
),
),
);
}
},
onCompleteOrderDetails: (context, configuration) async {
if (shoppingConfiguration.onCompleteOrderDetails != null) {
return shoppingConfiguration.onCompleteOrderDetails!(
context,
configuration,
);
} else {
shoppingConfiguration.shoppingService.shoppingCartService.clear();
return Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) => ShoppingProductPage(
shoppingConfiguration: shoppingConfiguration,
),
),
);
}
},
),
);
}

View file

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

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