feat(example): add amazon product page example

This commit is contained in:
markkiepe 2024-06-14 13:34:12 +02:00
parent e201120bc2
commit a38e97c68f
13 changed files with 763 additions and 0 deletions

53
example_amazon/.gitignore vendored Normal file
View file

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

16
example_amazon/README.md Normal file
View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,24 @@
import "package:flutter_product_page/flutter_product_page.dart";
import "package:flutter_shopping_cart/flutter_shopping_cart.dart";
class MyProduct extends ShoppingCartProduct with ProductPageProduct {
MyProduct({
required super.id,
required super.name,
required super.price,
required this.category,
required this.imageUrl,
});
@override
final String category;
@override
final String imageUrl;
@override
final double? discountPrice = 0.0;
@override
final bool hasDiscount = false;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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