From 7907dcf6c2ddadb21fe11d191555852b1e68fd7a Mon Sep 17 00:00:00 2001 From: Sergey Elpashev Date: Wed, 22 May 2024 15:46:09 +0300 Subject: [PATCH] Add: getting products from API --- lib/interfaces/items.dart | 80 ++++++++++ lib/pages/basket.dart | 243 ++++++++++++++++-------------- lib/pages/detail.dart | 145 +++++++++++------- lib/pages/main.dart | 95 ++++++------ lib/pages/order_confirmation.dart | 40 ++--- lib/tools/items.dart | 33 ++++ 6 files changed, 399 insertions(+), 237 deletions(-) create mode 100644 lib/interfaces/items.dart create mode 100644 lib/tools/items.dart diff --git a/lib/interfaces/items.dart b/lib/interfaces/items.dart new file mode 100644 index 0000000..c4af0d9 --- /dev/null +++ b/lib/interfaces/items.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; + +class GymItem { + final String id; + final String externalId; + final String title; + final String description; + final int count; + final double price; + final String categoryId; + final List images; + int localCount = 0; + + GymItem({ + required this.id, + required this.externalId, + required this.title, + required this.description, + required this.count, + required this.price, + required this.categoryId, + required this.images, + }); + + factory GymItem.fromRawJson(String str) => GymItem.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory GymItem.fromJson(Map json) => GymItem( + id: json["id"], + externalId: json["ExternalId"], + title: json["title"], + description: json["description"], + count: json["count"], + price: json["price"] / 100, + categoryId: json["categoryId"], + images: List.from( + json["images"].map((x) => GymImage.fromJson(x))), + ); + + Map toJson() => { + "id": id, + "ExternalId": externalId, + "title": title, + "description": description, + "count": count, + "price": price * 100, + "categoryId": categoryId, + "images": List.from(images.map((x) => x.toJson())), + }; +} + +class GymImage { + final String id; + final dynamic deletedAt; + final String url; + + GymImage({ + required this.id, + required this.deletedAt, + required this.url, + }); + + factory GymImage.fromRawJson(String str) => + GymImage.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory GymImage.fromJson(Map json) => GymImage( + id: json["id"], + deletedAt: json["deletedAt"], + url: json["url"], + ); + + Map toJson() => { + "id": id, + "deletedAt": deletedAt, + "url": url, + }; +} diff --git a/lib/pages/basket.dart b/lib/pages/basket.dart index de3240f..6ff485b 100644 --- a/lib/pages/basket.dart +++ b/lib/pages/basket.dart @@ -2,9 +2,10 @@ import 'package:flutter/material.dart'; import 'package:gymlink_module_web/components/app_bar.dart'; import 'package:gymlink_module_web/components/basket_item_card.dart'; import 'package:gymlink_module_web/components/heading.dart'; +import 'package:gymlink_module_web/interfaces/items.dart'; import 'package:gymlink_module_web/pages/order_confirmation.dart'; import 'package:gymlink_module_web/providers/cart.dart'; -import 'package:gymlink_module_web/providers/main.dart'; +import 'package:gymlink_module_web/tools/items.dart'; import 'package:gymlink_module_web/tools/prefs.dart'; import 'package:gymlink_module_web/tools/routes.dart'; import 'package:lazy_load_scrollview/lazy_load_scrollview.dart'; @@ -56,46 +57,47 @@ class BasketPage extends StatefulWidget { } class _BasketPageState extends State { - List> cartItems = []; - int totalPrice = 0; + List cartItems = []; + double totalPrice = 0; + List gymCart = []; @override void initState() { super.initState(); - getCart().then((value) { - setState(() { - cartItems = value.map((element) { - final item = cart.firstWhere((e) => e['id'] == element['id']); - return {...item, 'count': element['count'] as int}; - }).toList(); - totalPrice = cartItems.fold( - 0, - (sum, item) => - sum + int.parse(item['price']) * item['count'] as int); - }); - }); + Future.microtask(() => getCart().then((value) async { + final items = await getItems(context); + setState(() { + gymCart = items; + cartItems = value.map((element) { + final item = gymCart.firstWhere((e) => e.id == element['id']); + item.localCount = element['count'] as int; + return item; + }).toList(); + totalPrice = cartItems.fold( + 0, (sum, item) => sum + item.price * item.localCount); + }); + })); } void _updateCart() { Provider.of(context, listen: false).updateCartLength(); - Provider.of(context, listen: false).onTokenReceived('123'); } void removeItem(String id) async { - final item = cartItems.firstWhere((element) => element['id'] == id); + final item = cartItems.firstWhere((element) => element.id == id); bool toDelete = false; setState(() { - if (item['count'] > 1) { - item['count']--; - cartItems[cartItems.indexOf(item)]['count'] = item['count']; + if (item.localCount > 1) { + item.localCount--; + cartItems[cartItems.indexOf(item)].localCount = item.localCount; } else { toDelete = true; } - totalPrice = cartItems.fold(0, - (sum, item) => sum + int.parse(item['price']) * item['count'] as int); + totalPrice = + cartItems.fold(0, (sum, item) => sum + item.price * item.localCount); }); if (toDelete) { - await _deleteItemAlert(id, item['name']); + await _deleteItemAlert(id, item.title); } else { await removeItemFromCart(id); } @@ -103,15 +105,16 @@ class _BasketPageState extends State { void addItem(String id) async { setState(() { - final item = cartItems.firstWhere((element) => element['id'] == id, - orElse: () => { - ...cart.firstWhere((element) => element['id'] == id), - 'count': 0 - }); - item['count']++; - cartItems[cartItems.indexOf(item)]['count'] = item['count']; - totalPrice = cartItems.fold(0, - (sum, item) => sum + int.parse(item['price']) * item['count'] as int); + final item = + cartItems.firstWhere((element) => element.id == id, orElse: () { + final cartItem = gymCart.firstWhere((element) => element.id == id); + cartItem.localCount = 0; + return cartItem; + }); + item.localCount++; + cartItems[cartItems.indexOf(item)].localCount = item.localCount; + totalPrice = + cartItems.fold(0, (sum, item) => sum + item.price * item.localCount); }); await addItemToCart(id); } @@ -142,7 +145,7 @@ class _BasketPageState extends State { onPressed: () { removeItemFromCart(id); setState(() { - cartItems.removeWhere((element) => element['id'] == id); + cartItems.removeWhere((element) => element.id == id); }); if (mounted) { _updateCart(); @@ -224,76 +227,20 @@ class _BasketPageState extends State { body: Column( children: [ const GymLinkHeader(title: "Корзина"), - cartItems.isEmpty - ? Expanded( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('Корзина пуста', - style: Theme.of(context).textTheme.bodyLarge), - const SizedBox(height: 10), - ElevatedButton( - onPressed: () => Navigator.pop(context, true), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - shape: const RoundedRectangleBorder( - borderRadius: - BorderRadius.all(Radius.circular(50)), - ), - foregroundColor: Colors.white, - ), - child: const Text('Вернуться назад'), - ), - ], - ), - ), - ) - : Expanded( - child: _buildRowOrCol( - context: context, - children: [ - Expanded( - child: LazyLoadScrollView( - onEndOfPage: _onLoad, - child: ListView.builder( - itemCount: cartItems.length, - itemBuilder: (context, index) { - final item = cartItems[index]; - return BasketItemCard( - name: item['name'], - price: item['price'], - id: item['id'], - image: Image( - image: AssetImage('assets/${item['image']}'), - width: 50, - ), - onTapPlus: () => addItem(item['id'].toString()), - onTapMinus: () { - removeItem(item['id'].toString()); - }, - quantity: item['count'].toString(), - ); - }, - ), - ), - ), - _buildSpacer(), - Padding( - padding: const EdgeInsetsDirectional.symmetric( - horizontal: 10, vertical: 10), + gymCart.isEmpty + ? const Expanded( + child: Center(child: CircularProgressIndicator())) + : cartItems.isEmpty + ? Expanded( + child: Center( child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Text( - 'Итого: $totalPrice', - ), + Text('Корзина пуста', + style: Theme.of(context).textTheme.bodyLarge), + const SizedBox(height: 10), ElevatedButton( - onPressed: () => Navigator.of(context).push( - CustomPageRoute( - builder: (context) => - const OrderConfirmationPage(), - ), - ), + onPressed: () => Navigator.pop(context, true), style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).primaryColor, shape: const RoundedRectangleBorder( @@ -302,27 +249,89 @@ class _BasketPageState extends State { ), foregroundColor: Colors.white, ), - child: const Text('Оформить заказ'), - ), - const SizedBox(height: 10), - ElevatedButton( - onPressed: _clearCartAlert, - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - shape: const RoundedRectangleBorder( - borderRadius: - BorderRadius.all(Radius.circular(50))), - foregroundColor: Colors.white, - ), - child: const Text('Очистить корзину'), + child: const Text('Вернуться назад'), ), ], ), ), - const SizedBox(width: 50), - ], - ), - ), + ) + : Expanded( + child: _buildRowOrCol( + context: context, + children: [ + Expanded( + child: LazyLoadScrollView( + onEndOfPage: _onLoad, + child: ListView.builder( + itemCount: cartItems.length, + itemBuilder: (context, index) { + final item = cartItems[index]; + return BasketItemCard( + name: item.title, + price: item.price.toString(), + id: item.id, + image: Image( + image: NetworkImage(item.images[0].url), + width: 50, + ), + onTapPlus: () => + addItem(item.id.toString()), + onTapMinus: () { + removeItem(item.id.toString()); + }, + quantity: item.localCount.toString(), + ); + }, + ), + ), + ), + _buildSpacer(), + Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 10, vertical: 10), + child: Column( + children: [ + Text( + 'Итого: $totalPrice', + ), + ElevatedButton( + onPressed: () => Navigator.of(context).push( + CustomPageRoute( + builder: (context) => + const OrderConfirmationPage(), + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: + Theme.of(context).primaryColor, + shape: const RoundedRectangleBorder( + borderRadius: + BorderRadius.all(Radius.circular(50)), + ), + foregroundColor: Colors.white, + ), + child: const Text('Оформить заказ'), + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: _clearCartAlert, + style: ElevatedButton.styleFrom( + backgroundColor: + Theme.of(context).primaryColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(50))), + foregroundColor: Colors.white, + ), + child: const Text('Очистить корзину'), + ), + ], + ), + ), + const SizedBox(width: 50), + ], + ), + ), ], ), ); diff --git a/lib/pages/detail.dart b/lib/pages/detail.dart index 4675afa..2818e64 100644 --- a/lib/pages/detail.dart +++ b/lib/pages/detail.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:gymlink_module_web/components/app_bar.dart'; import 'package:gymlink_module_web/components/heading.dart'; +import 'package:gymlink_module_web/pages/basket.dart'; import 'package:gymlink_module_web/providers/cart.dart'; import 'package:gymlink_module_web/tools/prefs.dart'; +import 'package:gymlink_module_web/tools/routes.dart'; import 'package:provider/provider.dart'; //TODO: Сделать получение инфы через объект @@ -83,38 +85,59 @@ class _DetailPageState extends State { child: const Text('Добавить в корзину'), ); } else { - return Row( - mainAxisSize: MainAxisSize.min, + return Column( children: [ - IconButton( - icon: const Icon(Icons.remove), - onPressed: () async { - await removeItemFromCart(widget.id); - setState(() { - if (quantity > 1) { - quantity--; - } else { - isInCart = false; - quantity = 0; - } - }); - if (mounted) { - context.read().updateCartLength(); - } - }, - ), - const SizedBox(width: 10), - Text('$quantity'), - const SizedBox(width: 10), - IconButton( - icon: const Icon(Icons.add), - onPressed: () async { - await addItemToCart(widget.id); - setState(() { - quantity++; - }); - }, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.remove), + onPressed: () async { + await removeItemFromCart(widget.id); + setState(() { + if (quantity > 1) { + quantity--; + } else { + isInCart = false; + quantity = 0; + } + }); + if (mounted) { + context.read().updateCartLength(); + } + }, + ), + const SizedBox(width: 10), + Text('$quantity'), + const SizedBox(width: 10), + IconButton( + icon: const Icon(Icons.add), + onPressed: () async { + await addItemToCart(widget.id); + setState(() { + quantity++; + }); + }, + ), + ], ), + Padding( + padding: const EdgeInsets.only(top: 10), + child: ElevatedButton( + onPressed: () { + Navigator.pushReplacement(context, + CustomPageRoute(builder: (context) => const BasketPage())); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(50)), + ), + foregroundColor: Colors.white, + ), + child: const Text('Открыть корзину'), + ), + ) ], ); } @@ -139,37 +162,43 @@ class _DetailPageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ widget.image, - Padding( - padding: const EdgeInsetsDirectional.all(30), - child: ConstrainedBox( - constraints: const BoxConstraints( - minWidth: 340, - maxWidth: 340, - maxHeight: 600, - ), - child: Card( - elevation: 4, - color: Theme.of(context).scaffoldBackgroundColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsetsDirectional.all(15), - child: ConstrainedBox( - constraints: const BoxConstraints( - minHeight: 100, + widget.description != '' + ? Padding( + padding: const EdgeInsetsDirectional.all(30), + child: ConstrainedBox( + constraints: const BoxConstraints( + minWidth: 340, + maxWidth: 340, + maxHeight: 600, + ), + child: Card( + elevation: 4, + color: + Theme.of(context).scaffoldBackgroundColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), ), - child: Text( - widget.description, - style: Theme.of(context).textTheme.bodyMedium, + child: SingleChildScrollView( + child: Padding( + padding: + const EdgeInsetsDirectional.all(15), + child: ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 100, + ), + child: Text( + widget.description, + style: Theme.of(context) + .textTheme + .bodyMedium, + ), + ), + ), ), ), ), - ), - ), - ), - ), + ) + : const SizedBox.shrink(), Align( alignment: const AlignmentDirectional(0, -1), child: Padding( @@ -179,7 +208,7 @@ class _DetailPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - 'Стоимость ${widget.price}', + 'Стоимость ${widget.price}руб.', style: Theme.of(context).textTheme.bodyLarge, ), _buildButton() diff --git a/lib/pages/main.dart b/lib/pages/main.dart index 8e5898f..464d902 100644 --- a/lib/pages/main.dart +++ b/lib/pages/main.dart @@ -3,11 +3,12 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:gymlink_module_web/components/app_bar.dart'; import 'package:gymlink_module_web/components/item_card.dart'; +import 'package:gymlink_module_web/interfaces/items.dart'; import 'package:gymlink_module_web/pages/basket.dart'; import 'package:gymlink_module_web/pages/detail.dart'; import 'package:gymlink_module_web/pages/order_history.dart'; import 'package:gymlink_module_web/providers/cart.dart'; -import 'package:gymlink_module_web/providers/main.dart'; +import 'package:gymlink_module_web/tools/items.dart'; import 'package:gymlink_module_web/tools/prefs.dart'; import 'package:gymlink_module_web/tools/relative.dart'; import 'package:gymlink_module_web/tools/routes.dart'; @@ -65,22 +66,22 @@ class MainPage extends StatefulWidget { class _MainPageState extends State { String searchText = ''; - List> filteredData = []; + List filteredData = []; + List items = []; int cartLength = 0; @override void initState() { super.initState(); - filteredData = testData; getCart().then((value) { setState(() { cartLength = value.length; }); - }).whenComplete(() { - if (mounted) { - setState(() {}); - } }); + getItems(context).then((value) => setState(() { + filteredData = value; + items = value; + })); } Future _goToPage() async { @@ -97,8 +98,8 @@ class _MainPageState extends State { void _onSearch() { setState(() { - filteredData = testData - .where((element) => (element['name']!).contains(searchText)) + filteredData = items + .where((element) => (element.title).contains(searchText)) .toList(); }); } @@ -106,7 +107,6 @@ class _MainPageState extends State { @override Widget build(BuildContext context) { final cartL = context.watch().cartLength; - final onError = context.read().onError; return Scaffold( appBar: const GymLinkAppBar(), body: Column( @@ -160,7 +160,6 @@ class _MainPageState extends State { ), ElevatedButton( onPressed: () { - onError(); Navigator.of(context).push(CustomPageRoute( builder: (context) => const HistoryPage(), )); @@ -193,40 +192,48 @@ class _MainPageState extends State { onEndOfPage: _onLoad, child: Stack( children: [ - GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: min( - (MediaQuery.sizeOf(context).width ~/ 200).toInt(), 8), - ), - itemCount: filteredData.length, - itemBuilder: (context, index) { - final product = filteredData[index]; - return ProductCard( - imagePath: Image( - image: Image.network( - 'https://rus-sport.net/upload/iblock/311/topb85ez18pq0aavohpa5zipk2sbfxll.jpg') - .image, - width: 50, - ), - name: product['name']!, - price: product['price']!, - onTap: () => Navigator.of(context).push( - CustomPageRoute( - builder: (context) => DetailPage( - name: product['name']!, - description: product['details']!, - price: product['price']!, - id: product['id']!, - image: Image( - image: - AssetImage('assets/${product['image']!}'), - width: 300), + items.isEmpty + ? const Center(child: CircularProgressIndicator()) + : filteredData.isEmpty + ? const Center(child: Text('Ничего не найдено')) + : GridView.builder( + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: min( + (MediaQuery.sizeOf(context).width ~/ 200) + .toInt(), + 8), + ), + itemCount: filteredData.length, + itemBuilder: (context, index) { + final product = filteredData[index]; + return ProductCard( + imagePath: Image( + image: Image.network(product.images[0].url) + .image, + width: 50, + ), + name: product.title, + price: product.price.toString(), + onTap: () => Navigator.of(context).push( + CustomPageRoute( + builder: (context) => DetailPage( + name: product.title, + description: product.description, + price: product.price.toString(), + id: product.id, + image: Image( + image: Image.network( + product.images[0].url) + .image, + width: 300, + ), + ), + ), + ), + ); + }, ), - ), - ), - ); - }, - ), ], ), ), diff --git a/lib/pages/order_confirmation.dart b/lib/pages/order_confirmation.dart index 9b35e36..bd4424f 100644 --- a/lib/pages/order_confirmation.dart +++ b/lib/pages/order_confirmation.dart @@ -3,8 +3,10 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:gymlink_module_web/components/app_bar.dart'; import 'package:gymlink_module_web/components/heading.dart'; import 'package:gymlink_module_web/components/order_confirm_item_card.dart'; +import 'package:gymlink_module_web/interfaces/items.dart'; import 'package:gymlink_module_web/pages/order_history.dart'; import 'package:gymlink_module_web/providers/cart.dart'; +import 'package:gymlink_module_web/tools/items.dart'; import 'package:gymlink_module_web/tools/prefs.dart'; import 'package:gymlink_module_web/tools/routes.dart'; import 'package:provider/provider.dart'; @@ -56,24 +58,26 @@ class OrderConfirmationPage extends StatefulWidget { } class _OrderConfirmationPageState extends State { - List> cartItems = []; - int totalPrice = 0; + List cartItems = []; + double totalPrice = 0; + List gymCart = []; @override void initState() { super.initState(); - getCart().then((value) { - setState(() { - cartItems = value.map((element) { - final item = cart.firstWhere((e) => e['id'] == element['id']); - return {...item, 'count': element['count'] as int}; - }).toList(); - totalPrice = cartItems.fold( - 0, - (sum, item) => - sum + int.parse(item['price']) * item['count'] as int); - }); - }); + Future.microtask(() => getCart().then((value) async { + final items = await getItems(context); + setState(() { + gymCart = items; + cartItems = value.map((element) { + final item = gymCart.firstWhere((e) => e.id == element['id']); + item.localCount = element['count'] as int; + return item; + }).toList(); + totalPrice = cartItems.fold( + 0, (sum, item) => sum + item.price * item.localCount); + }); + })); } Future _goToPage() async { @@ -101,13 +105,13 @@ class _OrderConfirmationPageState extends State { itemBuilder: (context, index) { final item = cartItems[index]; return OrderConfirmItemCard( - name: item['name'], + name: item.title, image: Image( - image: AssetImage('assets/${item['image']}'), + image: NetworkImage(item.images[0].url), width: 50, height: 50), - price: double.parse(item['price']), - count: item['count'], + price: item.price, + count: item.localCount, ); }, ), diff --git a/lib/tools/items.dart b/lib/tools/items.dart new file mode 100644 index 0000000..cb79113 --- /dev/null +++ b/lib/tools/items.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:gymlink_module_web/interfaces/items.dart'; +import 'package:gymlink_module_web/providers/main.dart'; +import 'package:http/http.dart' as http; +import 'package:provider/provider.dart'; + +Future> getItems(BuildContext context) async { + final token = context.read().token; + if (token != '') { + final Uri url = + Uri.http('gymlink.freemyip.com:8080', 'api/product/get-list'); + try { + final response = await http.get(url, headers: { + 'Authorization': 'Bearer $token', + }); + if (response.statusCode == 200) { + final data = + jsonDecode(utf8.decode(response.bodyBytes)) as List; + final items = data.map((e) => GymItem.fromJson(e)).toList(); + return items; + } + throw Error(); + } catch (e) { + debugPrint('error: $e'); + return await Future.delayed( + const Duration(seconds: 5), () => getItems(context)); + } + } + context.read().onError(); + return []; +}