diff --git a/example_amazon/.gitignore b/example_amazon/.gitignore new file mode 100644 index 0000000..8760a85 --- /dev/null +++ b/example_amazon/.gitignore @@ -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/ diff --git a/example_amazon/README.md b/example_amazon/README.md new file mode 100644 index 0000000..eddfc9c --- /dev/null +++ b/example_amazon/README.md @@ -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. diff --git a/example_amazon/analysis_options.yaml b/example_amazon/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/example_amazon/analysis_options.yaml @@ -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 diff --git a/example_amazon/lib/main.dart b/example_amazon/lib/main.dart new file mode 100644 index 0000000..a48842e --- /dev/null +++ b/example_amazon/lib/main.dart @@ -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), + ); +} diff --git a/example_amazon/lib/src/configuration/shopping_configuration.dart b/example_amazon/lib/src/configuration/shopping_configuration.dart new file mode 100644 index 0000000..3688380 --- /dev/null +++ b/example_amazon/lib/src/configuration/shopping_configuration.dart @@ -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 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.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; + // }, + ); diff --git a/example_amazon/lib/src/models/my_category.dart b/example_amazon/lib/src/models/my_category.dart new file mode 100644 index 0000000..50d721a --- /dev/null +++ b/example_amazon/lib/src/models/my_category.dart @@ -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, + }); +} diff --git a/example_amazon/lib/src/models/my_product.dart b/example_amazon/lib/src/models/my_product.dart new file mode 100644 index 0000000..f5a5ab4 --- /dev/null +++ b/example_amazon/lib/src/models/my_product.dart @@ -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; +} diff --git a/example_amazon/lib/src/routes.dart b/example_amazon/lib/src/routes.dart new file mode 100644 index 0000000..82efd55 --- /dev/null +++ b/example_amazon/lib/src/routes.dart @@ -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( + (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(), + ), + ), + ], + ), +); diff --git a/example_amazon/lib/src/services/category_service.dart b/example_amazon/lib/src/services/category_service.dart new file mode 100644 index 0000000..7c74bae --- /dev/null +++ b/example_amazon/lib/src/services/category_service.dart @@ -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 categories = { + "Electronics": "Electronica", + "Smart phones": "Telefoons", + "TV's": "TV's", +}; + +List 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 getCategories() => [ + 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 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 []; + } +} diff --git a/example_amazon/lib/src/ui/homepage.dart b/example_amazon/lib/src/ui/homepage.dart new file mode 100644 index 0000000..46e1373 --- /dev/null +++ b/example_amazon/lib/src/ui/homepage.dart @@ -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), + ), + ), + ), + ); +} diff --git a/example_amazon/lib/src/utils/go_router.dart b/example_amazon/lib/src/utils/go_router.dart new file mode 100644 index 0000000..d7b239a --- /dev/null +++ b/example_amazon/lib/src/utils/go_router.dart @@ -0,0 +1,26 @@ +import "package:flutter/material.dart"; +import "package:go_router/go_router.dart"; + +CustomTransitionPage buildScreenWithFadeTransition({ + required BuildContext context, + required GoRouterState state, + required Widget child, +}) => + CustomTransitionPage( + key: state.pageKey, + child: child, + transitionsBuilder: (context, animation, secondaryAnimation, child) => + FadeTransition(opacity: animation, child: child), + ); + +CustomTransitionPage buildScreenWithoutTransition({ + required BuildContext context, + required GoRouterState state, + required Widget child, +}) => + CustomTransitionPage( + key: state.pageKey, + child: child, + transitionsBuilder: (context, animation, secondaryAnimation, child) => + child, + ); diff --git a/example_amazon/lib/src/utils/theme.dart b/example_amazon/lib/src/utils/theme.dart new file mode 100644 index 0000000..0342db9 --- /dev/null +++ b/example_amazon/lib/src/utils/theme.dart @@ -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, + ), + ), + ), + ); diff --git a/example_amazon/pubspec.yaml b/example_amazon/pubspec.yaml new file mode 100644 index 0000000..6d51de9 --- /dev/null +++ b/example_amazon/pubspec.yaml @@ -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