Compare commits

...

25 Commits

Author SHA1 Message Date
f182c80210 fix: price reduction on deletion 2024-06-25 22:51:14 +03:00
cefabd1c70 fix: img loading on changing search text 2024-06-25 22:36:18 +03:00
f1055d40a4 fix: remove spacer 2024-06-25 13:58:41 +03:00
cfe0184e3a fix: card width 2024-06-24 22:35:47 +03:00
faa52dcaa2 fix: interface fixes 2024-06-24 14:56:06 +03:00
946d2ada41 fix: some fixes in example app 2024-06-24 14:08:02 +03:00
7cb92a7b83 fix: delete toLocal duplicate 2024-06-19 15:38:52 +03:00
dd59a605ad fix: some 2024-06-18 21:19:23 +03:00
34c0ea5fa1 feat: clear btn in web 2024-06-18 20:47:54 +03:00
893b925a04 fix: ultra-fake orders 2024-06-18 20:40:45 +03:00
bdcd4507c2 feat: fake orders 2024-06-18 15:14:37 +03:00
fb5538ab79 fix: order info page 2024-06-17 16:59:10 +03:00
0a22b5c051 Feat: order info page 2024-06-16 23:33:38 +03:00
73fe273c75 Fix: some changes 2024-06-14 01:37:54 +03:00
f5e1407281 Fix: https url 2024-06-14 01:37:42 +03:00
27da063c34 Fix: some design fixes 2024-06-12 02:03:20 +03:00
c0c3ef2ca0 Fix: shorten name 2024-06-09 14:40:30 +03:00
9335e8e694 fix: some fixes 2024-06-09 12:47:59 +03:00
04ee6d1699 Add: gym themes 2024-06-07 14:57:40 +03:00
65c8f56e20 Fix: shorten text 2024-06-07 14:57:28 +03:00
0170505376 Fix: category colors and count elements 2024-06-04 23:13:21 +03:00
15105a7f33 Add: search by category 2024-06-04 16:58:53 +03:00
1e5b235a6c Enhancement: back button to main menu reload 2024-06-04 15:57:17 +03:00
eaa8b138a4 Add: category on detail page and searching by API 2024-06-04 15:29:41 +03:00
db39169907 Add: new item interfaces and category interface 2024-06-04 14:36:43 +03:00
28 changed files with 1493 additions and 410 deletions

View File

@@ -46,7 +46,7 @@ android {
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 21 minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion targetSdkVersion 34
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }

View File

@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:label="Example Gym App" android:label="Example Gym App"
android:name="${applicationName}" android:name="${applicationName}"
@@ -31,7 +32,6 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
<uses-permission android:name="android.permission.INTERNET" />
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and https://developer.android.com/training/package-visibility?hl=en and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT. https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

View File

@@ -22,6 +22,11 @@ class GymLinkAppBar extends StatelessWidget implements PreferredSizeWidget {
width: 24, width: 24,
height: 24, height: 24,
semanticsLabel: 'GymLink Logo', semanticsLabel: 'GymLink Logo',
colorFilter: ColorFilter.mode(
Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black,
BlendMode.srcIn),
), ),
), ),
Align( Align(

View File

@@ -6,7 +6,7 @@ class BasketItemCard extends StatelessWidget {
final String name; final String name;
final String price; final String price;
final String id; final String id;
final Image image; final Widget image;
final String quantity; final String quantity;
final VoidCallback onTapPlus; final VoidCallback onTapPlus;
final VoidCallback onTapMinus; final VoidCallback onTapMinus;

View File

@@ -1,8 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gymlink_module_web/pages/main.dart';
import 'package:gymlink_module_web/tools/routes.dart';
class GymLinkHeader extends StatelessWidget { class GymLinkHeader extends StatelessWidget {
final String title; final String title;
const GymLinkHeader({super.key, required this.title}); final bool toMain;
const GymLinkHeader({super.key, required this.title, this.toMain = false});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -13,7 +16,13 @@ class GymLinkHeader extends StatelessWidget {
Row( Row(
children: [ children: [
IconButton( IconButton(
onPressed: () => Navigator.pop(context), onPressed: () => toMain
? Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
builder: (context) => const MainPage()),
(route) => route.isFirst)
: Navigator.pop(context),
icon: const Icon(Icons.arrow_back)), icon: const Icon(Icons.arrow_back)),
Text(title, style: Theme.of(context).textTheme.titleLarge), Text(title, style: Theme.of(context).textTheme.titleLarge),
], ],

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:gymlink_module_web/pages/order_info.dart';
import 'package:gymlink_module_web/tools/routes.dart';
enum OrderStatus { created, inProgress, completed, canceled } enum OrderStatus { created, inProgress, completed, canceled }
@@ -14,15 +16,13 @@ class HistoryItemCard extends StatelessWidget {
final String id; final String id;
final String cost; final String cost;
final String date; final String date;
final Image image; final Widget image;
final OrderStatus status;
const HistoryItemCard({ const HistoryItemCard({
super.key, super.key,
required this.id, required this.id,
required this.cost, required this.cost,
required this.date, required this.date,
required this.status,
required this.image, required this.image,
}); });
@@ -38,6 +38,11 @@ class HistoryItemCard extends StatelessWidget {
minWidth: 600, minWidth: 600,
maxWidth: 800, maxWidth: 800,
), ),
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
CustomPageRoute(builder: (context) => OrderInfoPage(id: id)));
},
child: Card( child: Card(
elevation: 4, elevation: 4,
color: Theme.of(context).scaffoldBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,
@@ -62,8 +67,6 @@ class HistoryItemCard extends StatelessWidget {
MarkdownBody( MarkdownBody(
data: '### Заказ **№$id** от $date', data: '### Заказ **№$id** от $date',
), ),
MarkdownBody(
data: 'Статус: **${orderStatusMap[status]}**'),
MarkdownBody(data: 'Сумма: **$cost руб.**'), MarkdownBody(data: 'Сумма: **$cost руб.**'),
], ],
) )
@@ -74,6 +77,7 @@ class HistoryItemCard extends StatelessWidget {
), ),
), ),
), ),
),
); );
} }
} }

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ProductCard extends StatelessWidget { class ProductCard extends StatelessWidget {
final Image imagePath; final Widget imagePath;
final String name; final String name;
final String price; final String price;
final VoidCallback onTap; final VoidCallback onTap;
@@ -15,10 +15,10 @@ class ProductCard extends StatelessWidget {
}); });
double getCardHeight({required BuildContext context}) { double getCardHeight({required BuildContext context}) {
if (MediaQuery.of(context).size.width > 600) { if (MediaQuery.of(context).size.width > 400) {
return 200; return 300;
} }
return 100; return 160;
} }
@override @override
@@ -27,7 +27,10 @@ class ProductCard extends StatelessWidget {
onTap: onTap, onTap: onTap,
child: ConstrainedBox( child: ConstrainedBox(
constraints: BoxConstraints( constraints: BoxConstraints(
minHeight: 80, maxHeight: getCardHeight(context: context)), minHeight: 160,
maxHeight: getCardHeight(context: context),
minWidth: 180,
maxWidth: 250),
child: Card( child: Card(
elevation: 3, elevation: 3,
color: Theme.of(context).scaffoldBackgroundColor, color: Theme.of(context).scaffoldBackgroundColor,
@@ -42,7 +45,8 @@ class ProductCard extends StatelessWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
name, name,
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleMedium,
maxLines: 2,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(

View File

@@ -5,7 +5,7 @@ class OrderConfirmItemCard extends StatelessWidget {
final String name; final String name;
final int count; final int count;
final double price; final double price;
final Image image; final Widget image;
const OrderConfirmItemCard({ const OrderConfirmItemCard({
super.key, super.key,
@@ -46,7 +46,8 @@ class OrderConfirmItemCard extends StatelessWidget {
name, name,
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
), ),
Text('$price руб. x $count = ${price * count} руб.'), Text(
'${price.toStringAsFixed(2)} руб. x $count = ${(price * count).toStringAsFixed(2)} руб.'),
], ],
) )
], ],

View File

@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
class OrderDetailCardItemCard extends StatelessWidget {
final String name;
final int count;
final double price;
final Widget image;
const OrderDetailCardItemCard(
{super.key,
required this.image,
required this.name,
required this.count,
required this.price});
@override
Widget build(BuildContext context) {
return Padding(
padding:
const EdgeInsetsDirectional.symmetric(horizontal: 10, vertical: 10),
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 130),
child: Card(
elevation: 4,
color: Theme.of(context).scaffoldBackgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
child: Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
image,
const SizedBox(width: 20),
Column(
mainAxisSize: MainAxisSize.min,
children: [
MarkdownBody(data: '# $name'),
Text(
'${price.toStringAsFixed(2)} руб. x $count = ${(price * count).toStringAsFixed(2)} руб.'),
],
)
],
),
MarkdownBody(data: '# X$count')
],
),
),
),
),
);
}
}

View File

@@ -1,5 +1,27 @@
import 'dart:convert'; import 'dart:convert';
class ItemsDataResponse {
final List<GymItem> rows;
ItemsDataResponse({
required this.rows,
});
factory ItemsDataResponse.fromRawJson(String str) =>
ItemsDataResponse.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory ItemsDataResponse.fromJson(Map<String, dynamic> json) =>
ItemsDataResponse(
rows: List<GymItem>.from(json["rows"].map((x) => GymItem.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"rows": List<dynamic>.from(rows.map((x) => x.toJson())),
};
}
class GymItem { class GymItem {
final String id; final String id;
final String externalId; final String externalId;
@@ -9,6 +31,8 @@ class GymItem {
final double price; final double price;
final String categoryId; final String categoryId;
final List<GymImage> images; final List<GymImage> images;
final String supplierId;
final String supplierName;
int localCount = 0; int localCount = 0;
GymItem({ GymItem({
@@ -20,13 +44,16 @@ class GymItem {
required this.price, required this.price,
required this.categoryId, required this.categoryId,
required this.images, required this.images,
required this.supplierId,
required this.supplierName,
}); });
factory GymItem.fromRawJson(String str) => GymItem.fromJson(json.decode(str)); factory GymItem.fromRawJson(String str) => GymItem.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson()); String toRawJson() => json.encode(toJson());
factory GymItem.fromJson(Map<String, dynamic> json) => GymItem( factory GymItem.fromJson(Map<String, dynamic> json) {
return GymItem(
id: json["id"], id: json["id"],
externalId: json["ExternalId"], externalId: json["ExternalId"],
title: json["title"], title: json["title"],
@@ -34,9 +61,14 @@ class GymItem {
count: json["count"], count: json["count"],
price: json["price"] / 100, price: json["price"] / 100,
categoryId: json["categoryId"], categoryId: json["categoryId"],
images: List<GymImage>.from( images:
json["images"].map((x) => GymImage.fromJson(x))), List<GymImage>.from(json["images"].map((x) => GymImage.fromJson(x))),
supplierId: json["supplier"] == null ? '' : json["supplier"]["id"] ?? '',
supplierName: json["supplier"] == null
? ''
: json["supplier"]["title"] ?? "Поставщик",
); );
}
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
"id": id, "id": id,
@@ -44,9 +76,11 @@ class GymItem {
"title": title, "title": title,
"description": description, "description": description,
"count": count, "count": count,
"price": price * 100, "price": price,
"categoryId": categoryId, "categoryId": categoryId,
"images": List<dynamic>.from(images.map((x) => x.toJson())), "images": List<dynamic>.from(images.map((x) => x.toJson())),
"supplier":
supplierId == '' ? null : {"id": supplierId, "title": supplierName},
}; };
} }
@@ -78,3 +112,214 @@ class GymImage {
"url": url, "url": url,
}; };
} }
class CategoryDataResponse {
final List<GymCategory> rows;
CategoryDataResponse({
required this.rows,
});
factory CategoryDataResponse.fromRawJson(String str) =>
CategoryDataResponse.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory CategoryDataResponse.fromJson(Map<String, dynamic> json) =>
CategoryDataResponse(
rows: List<GymCategory>.from(
json["rows"].map((x) => GymCategory.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"rows": List<dynamic>.from(rows.map((x) => x.toJson())),
};
}
class GymCategory {
final String id;
final String name;
GymCategory({
required this.id,
required this.name,
});
factory GymCategory.fromRawJson(String str) =>
GymCategory.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory GymCategory.fromJson(Map<String, dynamic> json) => GymCategory(
id: json["id"],
name: json["name"],
);
Map<String, dynamic> toJson() => {
"id": id,
"name": name,
};
}
class GymHistoryItem {
String id;
final String date;
final String sum;
final String photo;
final String timestamp;
GymHistoryItem(
{required this.id,
required this.date,
required this.sum,
required this.photo,
required this.timestamp});
factory GymHistoryItem.fromRawJson(String str) =>
GymHistoryItem.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory GymHistoryItem.fromJson(Map<String, dynamic> json) => GymHistoryItem(
id: json["id"],
date: json["date"],
sum: json["sum"],
photo: json["photo"],
timestamp: json["timestamp"],
);
Map<String, dynamic> toJson() => {
"id": id,
"date": date,
"sum": sum,
"timestamp": timestamp,
"photo": photo,
};
}
class GymHistoryItemDetail {
String id;
final String date;
final String sum;
String? payUrl;
final String receiver;
final String email;
final String timestamp;
final String address;
final List<GymHistoryItemDetailProvider> providers;
GymHistoryItemDetail(
{required this.id,
required this.date,
required this.sum,
this.payUrl,
required this.providers,
required this.receiver,
required this.email,
required this.address,
required this.timestamp});
factory GymHistoryItemDetail.fromRawJson(String str) =>
GymHistoryItemDetail.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory GymHistoryItemDetail.fromJson(Map<String, dynamic> json) =>
GymHistoryItemDetail(
id: json["id"],
date: json["date"],
sum: json["sum"],
receiver: json["receiver"],
email: json["email"],
address: json["address"],
payUrl: json["pay_url"] as String?,
providers: List<GymHistoryItemDetailProvider>.from(json["providers"]
.map((x) => GymHistoryItemDetailProvider.fromJson(x))),
timestamp: json["timestamp"]);
Map<String, dynamic> toJson() => {
"id": id,
"date": date,
"sum": sum,
"pay_url": payUrl,
"providers": List<dynamic>.from(providers.map((x) => x.toJson())),
"receiver": receiver,
"email": email,
"timestamp": timestamp,
"address": address,
};
}
class GymHistoryItemDetailProvider {
final String id;
final String name;
String status;
final List<GymHistoryItemDetailItem> items;
GymHistoryItemDetailProvider({
required this.id,
required this.name,
required this.status,
required this.items,
});
factory GymHistoryItemDetailProvider.fromRawJson(String str) =>
GymHistoryItemDetailProvider.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory GymHistoryItemDetailProvider.fromJson(Map<String, dynamic> json) {
return GymHistoryItemDetailProvider(
id: json["id"],
name: json["name"],
status: json["status"],
items: List<GymHistoryItemDetailItem>.from(
json["items"].map((x) => GymHistoryItemDetailItem.fromJson(x))),
);
}
Map<String, dynamic> toJson() => {
"id": id,
"name": name,
"status": status,
"items": List<dynamic>.from(items.map((x) => x.toJson())),
};
}
class GymHistoryItemDetailItem {
final String photo;
final String id;
final int count;
final String name;
final String price;
GymHistoryItemDetailItem({
required this.photo,
required this.id,
required this.count,
required this.price,
required this.name,
});
factory GymHistoryItemDetailItem.fromRawJson(String str) =>
GymHistoryItemDetailItem.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory GymHistoryItemDetailItem.fromJson(Map<String, dynamic> json) =>
GymHistoryItemDetailItem(
photo: json["photo"],
id: json["id"],
count: json["count"],
price: json["price"],
name: json["name"],
);
Map<String, dynamic> toJson() => {
"photo": photo,
"id": id,
"count": count,
"price": price,
"name": name,
};
}

View File

@@ -2,18 +2,19 @@ import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gymlink_module_web/main_mobile.dart'; import 'package:gymlink_module_web/main_mobile.dart';
import 'package:gymlink_module_web/providers/main.dart'; import 'package:gymlink_module_web/providers/main.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() { void main() {
runApp(const MyExampleApp()); runApp(const MyExampleApp());
} }
Future<String> getToken(String token, String clientId) async { Future<String> getToken(String token, String clientId) async {
debugPrint(token); var url = Uri.https('gymlink.freemyip.com', 'api/auth/authorize_client');
var url = Uri.http('gymlink.freemyip.com:8080', 'api/auth/authorize_client');
try { try {
var response = await http.post(url, var response = await http.post(url,
body: {'GymKey': token, 'id': clientId}); // Just testing token body: {'GymKey': token, 'id': clientId}); // Just testing token
@@ -32,6 +33,8 @@ class MyExampleApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
return MaterialApp( return MaterialApp(
title: 'GymLink Example App', title: 'GymLink Example App',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
@@ -55,16 +58,32 @@ Widget getDrawer(BuildContext context) => Drawer(
ListTile( ListTile(
leading: const Icon(Icons.home), leading: const Icon(Icons.home),
title: const Text('Home'), title: const Text('Home'),
onTap: () => Navigator.of(context).push( onTap: () {
Future.microtask(() async {
final prefs = await SharedPreferences.getInstance();
prefs.remove('token');
prefs.remove('history');
prefs.remove('cart');
prefs.remove('detail_history');
});
Navigator.of(context).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const ExampleMainPage(), builder: (context) => const ExampleMainPage(),
), ),
), );
), }),
ListTile( ListTile(
leading: const Icon(Icons.sell), leading: const Icon(Icons.sell),
title: const Text('Club 2'), title: const Text('Club 2'),
onTap: () => Navigator.of(context).push( onTap: () {
Future.microtask(() async {
final prefs = await SharedPreferences.getInstance();
prefs.remove('token');
prefs.remove('history');
prefs.remove('cart');
prefs.remove('detail_history');
});
Navigator.of(context).pushReplacement(
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ChangeNotifierProvider( builder: (context) => ChangeNotifierProvider(
create: (_) => GymLinkProvider(), create: (_) => GymLinkProvider(),
@@ -73,12 +92,13 @@ Widget getDrawer(BuildContext context) => Drawer(
), ),
), ),
), ),
), );
), }),
ListTile( ListTile(
leading: const Icon(Icons.search), leading: const Icon(Icons.search),
title: const Text('Example page'), title: const Text('Example page'),
onTap: () => Navigator.of(context).push(MaterialPageRoute( onTap: () =>
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => const ExampleSecondPage(), builder: (context) => const ExampleSecondPage(),
)), )),
), ),
@@ -129,7 +149,7 @@ class _ExamplePageState extends State<ExamplePage> {
Future<void> _setToken() async { Future<void> _setToken() async {
final token = await getToken('eeb42dcb-8e5b-4f21-825a-3fc7ada43445', '123'); final token = await getToken('eeb42dcb-8e5b-4f21-825a-3fc7ada43445', '123');
if (token != '') { if (token != '') {
context.read<GymLinkProvider>().onTokenReceived(token); context.read<GymLinkProvider>().checkToken(token);
} else { } else {
context.read<GymLinkProvider>().onError(); context.read<GymLinkProvider>().onError();
} }
@@ -139,17 +159,18 @@ class _ExamplePageState extends State<ExamplePage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('GymLink Example App'), title: const Text('GymLink Example App Gym 1'),
), ),
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
drawer: getDrawer(context), drawer: getDrawer(context),
body: Column( body: Column(
children: [ children: [
const Text('test'),
IconButton( IconButton(
icon: const Icon(Icons.abc), icon: const Icon(Icons.colorize),
onPressed: () { onPressed: () {
context.read<GymLinkProvider>().onTokenReceived('token123'); context.read<GymLinkProvider>().changeTheme(
Random().nextInt(0xffffff + 1),
blackTheme: Random().nextBool());
}, },
), ),
const Expanded( const Expanded(
@@ -161,14 +182,6 @@ class _ExamplePageState extends State<ExamplePage> {
const Text('Bottom text') const Text('Bottom text')
], ],
), ),
floatingActionButton: IconButton(
icon: const Icon(Icons.search),
onPressed: () {
context
.read<GymLinkProvider>()
.changeTheme(Random().nextInt(0xffffff + 1));
},
),
); );
} }
} }
@@ -209,8 +222,9 @@ class _ExampleClub2PageState extends State<ExampleClub2Page> {
Future<void> _setToken() async { Future<void> _setToken() async {
final token = await getToken('a8622a61-3142-487e-8db8-b6aebd4f04aa', '123'); final token = await getToken('a8622a61-3142-487e-8db8-b6aebd4f04aa', '123');
context.read<GymLinkProvider>().changeTheme(0xFFAABCAB);
if (token != '') { if (token != '') {
context.read<GymLinkProvider>().onTokenReceived(token); context.read<GymLinkProvider>().checkToken(token);
} else { } else {
context.read<GymLinkProvider>().onError(); context.read<GymLinkProvider>().onError();
} }
@@ -220,36 +234,22 @@ class _ExampleClub2PageState extends State<ExampleClub2Page> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('GymLink Example App'), title: const Text('GymLink Example App Gym2'),
), ),
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
drawer: getDrawer(context), drawer: getDrawer(context),
body: Column( body: const Column(
children: [ children: [
const Text('test'), Text('test'),
IconButton( Expanded(
icon: const Icon(Icons.abc),
onPressed: () {
context.read<GymLinkProvider>().onTokenReceived('token123');
},
),
const Expanded(
child: MyApp(), child: MyApp(),
), ),
const SizedBox( SizedBox(
height: 20, height: 20,
), ),
const Text('Bottom text') Text('Bottom text')
], ],
), ),
floatingActionButton: IconButton(
icon: const Icon(Icons.search),
onPressed: () {
context
.read<GymLinkProvider>()
.changeTheme(Random().nextInt(0xffffff + 1));
},
),
); );
} }
} }
@@ -264,8 +264,16 @@ class ExampleSecondPage extends StatelessWidget {
title: const Text('GymLink Example App'), title: const Text('GymLink Example App'),
), ),
drawer: getDrawer(context), drawer: getDrawer(context),
body: const Center( body: Center(
child: Text('Example page'), child: TextButton(
onPressed: () async {
final prefs = await SharedPreferences.getInstance();
prefs.remove('token');
prefs.remove('history');
prefs.remove('cart');
prefs.remove('detail_history');
},
child: const Text('Clear')),
), ),
); );
} }

View File

@@ -3,11 +3,13 @@ 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/basket_item_card.dart';
import 'package:gymlink_module_web/components/heading.dart'; import 'package:gymlink_module_web/components/heading.dart';
import 'package:gymlink_module_web/interfaces/items.dart'; import 'package:gymlink_module_web/interfaces/items.dart';
import 'package:gymlink_module_web/pages/main.dart';
import 'package:gymlink_module_web/pages/order_confirmation.dart'; import 'package:gymlink_module_web/pages/order_confirmation.dart';
import 'package:gymlink_module_web/providers/cart.dart'; import 'package:gymlink_module_web/providers/cart.dart';
import 'package:gymlink_module_web/tools/items.dart'; import 'package:gymlink_module_web/tools/items.dart';
import 'package:gymlink_module_web/tools/prefs.dart'; import 'package:gymlink_module_web/tools/prefs.dart';
import 'package:gymlink_module_web/tools/routes.dart'; import 'package:gymlink_module_web/tools/routes.dart';
import 'package:gymlink_module_web/tools/text.dart';
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart'; import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -108,13 +110,16 @@ class _BasketPageState extends State<BasketPage> {
} }
void addItem(String id) async { void addItem(String id) async {
setState(() {
final item = final item =
cartItems.firstWhere((element) => element.id == id, orElse: () { cartItems.firstWhere((element) => element.id == id, orElse: () {
final cartItem = gymCart.firstWhere((element) => element.id == id); final cartItem = gymCart.firstWhere((element) => element.id == id);
cartItem.localCount = 0; cartItem.localCount = 0;
return cartItem; return cartItem;
}); });
if (item.localCount + 1 > item.count) {
return;
}
setState(() {
item.localCount++; item.localCount++;
cartItems[cartItems.indexOf(item)].localCount = item.localCount; cartItems[cartItems.indexOf(item)].localCount = item.localCount;
totalPrice = totalPrice =
@@ -150,6 +155,8 @@ class _BasketPageState extends State<BasketPage> {
removeItemFromCart(id); removeItemFromCart(id);
setState(() { setState(() {
cartItems.removeWhere((element) => element.id == id); cartItems.removeWhere((element) => element.id == id);
totalPrice = cartItems.fold(
0, (sum, item) => sum + item.price * item.localCount);
}); });
if (mounted) { if (mounted) {
_updateCart(); _updateCart();
@@ -244,7 +251,11 @@ class _BasketPageState extends State<BasketPage> {
style: Theme.of(context).textTheme.bodyLarge), style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 10), const SizedBox(height: 10),
ElevatedButton( ElevatedButton(
onPressed: () => Navigator.pop(context, true), onPressed: () => Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
builder: (_) => const MainPage()),
(route) => route.isFirst),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
@@ -271,12 +282,25 @@ class _BasketPageState extends State<BasketPage> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = cartItems[index]; final item = cartItems[index];
return BasketItemCard( return BasketItemCard(
name: item.title, name: shortString(item.title),
price: item.price.toString(), price: item.price.toStringAsFixed(2),
id: item.id, id: item.id,
image: Image( image: FutureBuilder(
image: NetworkImage(item.images[0].url), future: precacheImage(
NetworkImage(item.images[0].url),
context),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
return Image(
image: NetworkImage(
item.images[0].url),
width: 50, width: 50,
);
} else {
return const CircularProgressIndicator();
}
},
), ),
onTapPlus: () => onTapPlus: () =>
addItem(item.id.toString()), addItem(item.id.toString()),
@@ -289,14 +313,14 @@ class _BasketPageState extends State<BasketPage> {
), ),
), ),
), ),
_buildSpacer(), // _buildSpacer(),
Padding( Padding(
padding: const EdgeInsetsDirectional.symmetric( padding: const EdgeInsetsDirectional.symmetric(
horizontal: 10, vertical: 10), horizontal: 10, vertical: 10),
child: Column( child: Column(
children: [ children: [
Text( Text(
'Итого: $totalPrice руб.', 'Итого: ${totalPrice.toStringAsFixed(2)} руб.',
), ),
ElevatedButton( ElevatedButton(
onPressed: () => Navigator.of(context).push( onPressed: () => Navigator.of(context).push(
@@ -332,7 +356,7 @@ class _BasketPageState extends State<BasketPage> {
], ],
), ),
), ),
const SizedBox(width: 50), // const SizedBox(width: 50),
], ],
), ),
), ),

View File

@@ -3,14 +3,17 @@ import 'dart:math';
import 'package:carousel_slider/carousel_slider.dart'; import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:gymlink_module_web/components/app_bar.dart'; import 'package:gymlink_module_web/components/app_bar.dart';
import 'package:gymlink_module_web/components/heading.dart'; import 'package:gymlink_module_web/components/heading.dart';
import 'package:gymlink_module_web/interfaces/items.dart'; import 'package:gymlink_module_web/interfaces/items.dart';
import 'package:gymlink_module_web/pages/basket.dart'; import 'package:gymlink_module_web/pages/basket.dart';
import 'package:gymlink_module_web/providers/cart.dart'; import 'package:gymlink_module_web/providers/cart.dart';
import 'package:gymlink_module_web/providers/main.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/prefs.dart';
import 'package:gymlink_module_web/tools/routes.dart'; import 'package:gymlink_module_web/tools/routes.dart';
import 'package:gymlink_module_web/tools/text.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -29,6 +32,7 @@ class _DetailPageState extends State<DetailPage> {
bool isInCart = false; bool isInCart = false;
int quantity = 0; int quantity = 0;
GymItem? item; GymItem? item;
String? categoryName;
final CarouselController _carouselController = CarouselController(); final CarouselController _carouselController = CarouselController();
int _currentImage = 0; int _currentImage = 0;
@@ -47,21 +51,35 @@ class _DetailPageState extends State<DetailPage> {
super.initState(); super.initState();
} }
void _getItem() async { Future<void> _getItem() async {
final Uri url = final Uri url =
Uri.http('gymlink.freemyip.com:8080', 'api/product/get/${widget.id}'); Uri.https('gymlink.freemyip.com', 'api/product/get/${widget.id}');
final response = await http.get(url, headers: { final response = await http.get(url, headers: {
'Authorization': 'Bearer ${context.read<GymLinkProvider>().token}', 'Authorization': 'Bearer ${context.read<GymLinkProvider>().token}',
}); });
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data =
GymItem.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
setState(() { setState(() {
item = GymItem.fromJson(jsonDecode(utf8.decode(response.bodyBytes))); item = data;
}); });
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
for (var element in item!.images) { for (var element in item!.images) {
precacheImage(NetworkImage(element.url), context); precacheImage(NetworkImage(element.url), context);
} }
}); });
if (mounted) {
getCategories(context).then((value) {
setState(() {
categoryName = value
.firstWhere(
(element) => element.id == (item!.categoryId),
orElse: () => GymCategory(id: item!.categoryId, name: ''),
)
.name;
});
});
}
} }
} }
@@ -70,12 +88,6 @@ class _DetailPageState extends State<DetailPage> {
required BuildContext context, required BuildContext context,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.spaceAround, MainAxisAlignment mainAxisAlignment = MainAxisAlignment.spaceAround,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center}) { CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center}) {
// if (false && MediaQuery.of(context).size.width > 600) {
// return Row(
// mainAxisAlignment: mainAxisAlignment,
// crossAxisAlignment: crossAxisAlignment,
// children: children);
// }
return Column( return Column(
mainAxisAlignment: mainAxisAlignment, mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment, crossAxisAlignment: crossAxisAlignment,
@@ -133,10 +145,12 @@ class _DetailPageState extends State<DetailPage> {
IconButton( IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
onPressed: () async { onPressed: () async {
if (item!.count > quantity) {
await addItemToCart(widget.id); await addItemToCart(widget.id);
setState(() { setState(() {
quantity++; quantity++;
}); });
}
}, },
), ),
], ],
@@ -175,14 +189,13 @@ class _DetailPageState extends State<DetailPage> {
appBar: const GymLinkAppBar(), appBar: const GymLinkAppBar(),
body: item != null body: item != null
? Column(mainAxisAlignment: MainAxisAlignment.start, children: [ ? Column(mainAxisAlignment: MainAxisAlignment.start, children: [
GymLinkHeader(title: item!.title), GymLinkHeader(title: shortString(item!.title, length: 20)),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: SizedBox( child: SizedBox(
width: MediaQuery.sizeOf(context).width, width: MediaQuery.sizeOf(context).width,
// height: MediaQuery.sizeOf(context).height,
child: _buildRowOrCol( child: _buildRowOrCol(
context: context, context: context,
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
@@ -246,9 +259,37 @@ class _DetailPageState extends State<DetailPage> {
}).toList(), }).toList(),
), ),
]) ])
: Image.network(item!.images[0].url, : Image.network(
width: min( item!.images[0].url,
550, MediaQuery.sizeOf(context).width)), height: 400,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Text(
item!.title,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Chip(
label: Text(categoryName != null
? (categoryName == ""
? "Без категории"
: categoryName!)
: ''),
backgroundColor: Colors.white,
labelStyle:
const TextStyle(color: Colors.black),
),
),
),
Center(
child: MarkdownBody(
data: '### Остаток: _${item!.count}_',
)),
item!.description != '' item!.description != ''
? Padding( ? Padding(
padding: const EdgeInsetsDirectional.all(30), padding: const EdgeInsetsDirectional.all(30),
@@ -296,7 +337,7 @@ class _DetailPageState extends State<DetailPage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
'Стоимость ${item!.price}руб.', 'Стоимость ${item!.price.toStringAsFixed(2)}руб.',
style: style:
Theme.of(context).textTheme.bodyLarge, Theme.of(context).textTheme.bodyLarge,
), ),

View File

@@ -10,11 +10,10 @@ import 'package:gymlink_module_web/pages/order_history.dart';
import 'package:gymlink_module_web/providers/cart.dart'; import 'package:gymlink_module_web/providers/cart.dart';
import 'package:gymlink_module_web/tools/items.dart'; import 'package:gymlink_module_web/tools/items.dart';
import 'package:gymlink_module_web/tools/prefs.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'; import 'package:gymlink_module_web/tools/routes.dart';
import 'package:gymlink_module_web/tools/text.dart';
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart'; import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
const List<Map<String, String>> testData = [ const List<Map<String, String>> testData = [
{ {
@@ -67,10 +66,14 @@ class MainPage extends StatefulWidget {
class _MainPageState extends State<MainPage> { class _MainPageState extends State<MainPage> {
String searchText = ''; String searchText = '';
List<GymItem> filteredData = []; List<GymItem> filteredData = [];
List<GymItem> items = [];
int cartLength = 0; int cartLength = 0;
int itemViewCount = 0; int itemViewCount = 0;
bool isLoading = false; bool isLoading = false;
bool isSearching = false;
List<GymCategory> categories = [];
GymCategory? selectedCategory;
final ScrollController _scrollController = ScrollController();
final TextEditingController _searchField = TextEditingController();
@override @override
void initState() { void initState() {
@@ -80,25 +83,12 @@ class _MainPageState extends State<MainPage> {
cartLength = value.length; cartLength = value.length;
}); });
}); });
getItems(context).then((value) => setState(() { getCategories(context).then((value) => setState(() {
filteredData = value; categories = value;
items = value; _onSearch();
itemViewCount = min(5, value.length);
WidgetsBinding.instance.addPostFrameCallback((_) {
for (var element in filteredData.sublist(0, itemViewCount)) {
precacheImage(NetworkImage(element.images[0].url), context);
}
});
})); }));
} }
Future<void> _goToPage() async {
final Uri url = Uri.parse('https://google.com');
if (!await launchUrl(url, webOnlyWindowName: '_blank')) {
throw 'Could not launch $url';
}
}
void _onLoad() async { void _onLoad() async {
if (itemViewCount < filteredData.length) { if (itemViewCount < filteredData.length) {
setState(() { setState(() {
@@ -112,18 +102,32 @@ class _MainPageState extends State<MainPage> {
} }
} }
void _onSearch() { void _searchItems({String searchText = '', String categoryId = ''}) async {
setState(() { setState(() {
filteredData = items isSearching = true;
.where((element) => (element.title).contains(searchText)) });
.toList(); final data =
itemViewCount = min(filteredData.length, itemViewCount); await getItems(context, searchText: searchText, categoryId: categoryId);
setState(() {
filteredData = data;
itemViewCount = min(filteredData.length, 5);
}); });
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
for (var element in filteredData.sublist(0, itemViewCount)) { for (var element in filteredData.sublist(0, itemViewCount)) {
precacheImage(NetworkImage(element.images[0].url), context); precacheImage(NetworkImage(element.images[0].url), context);
} }
}); });
setState(() {
isSearching = false;
});
}
void _onSearch() {
final categoryId = selectedCategory == null ? '' : selectedCategory!.id;
setState(() {
searchText = _searchField.text.trim().toLowerCase();
});
_searchItems(searchText: searchText, categoryId: categoryId);
} }
@override @override
@@ -141,19 +145,22 @@ class _MainPageState extends State<MainPage> {
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
onChanged: (value) => setState(() { onChanged: (value) {
searchText = value; searchText = value.trim().toLowerCase();
if (searchText == '') { if (searchText == '') {
_onSearch(); _onSearch();
} }
}), },
controller: _searchField,
textInputAction: TextInputAction.search,
onSubmitted: (_) => _onSearch(),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Поиск', hintText: 'Поиск',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(50),
), ),
suffixIcon: Padding( suffixIcon: Padding(
padding: const EdgeInsets.only(right: 8), padding: const EdgeInsets.only(right: 5),
child: ElevatedButton( child: ElevatedButton(
onPressed: _onSearch, onPressed: _onSearch,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@@ -176,7 +183,7 @@ class _MainPageState extends State<MainPage> {
), ),
), ),
), ),
getSpacer(context: context, flex: 2), // getSpacer(context: context, flex: 2),
const SizedBox( const SizedBox(
width: 8, width: 8,
), ),
@@ -209,17 +216,57 @@ class _MainPageState extends State<MainPage> {
], ],
), ),
), ),
Align(
alignment: Alignment.centerLeft,
child: SizedBox(
height: 60,
child: ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
return GestureDetector(
onTap: () {
setState(() {
selectedCategory =
selectedCategory == category ? null : category;
});
_onSearch();
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 10),
child: Chip(
label: Text(category.name),
backgroundColor: selectedCategory == category
? Theme.of(context).primaryColor
: Colors.white,
labelStyle: TextStyle(
color: selectedCategory == category
? Colors.white
: Colors.black),
),
),
);
}),
),
),
Expanded( Expanded(
child: LazyLoadScrollView( child: LazyLoadScrollView(
onEndOfPage: _onLoad, onEndOfPage: _onLoad,
isLoading: isLoading, isLoading: isLoading,
child: Scrollbar( child: Scrollbar(
controller: _scrollController,
child: ListView( child: ListView(
controller: _scrollController,
children: [ children: [
items.isEmpty filteredData.isEmpty &&
? const Center(child: CircularProgressIndicator()) (searchText != '' || selectedCategory != null) &&
: filteredData.isEmpty !isSearching
? const Center(child: Text('Ничего не найдено')) ? const Center(child: Text('Ничего не найдено'))
: isSearching
? const Center(child: CircularProgressIndicator())
: GridView.builder( : GridView.builder(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
@@ -227,22 +274,35 @@ class _MainPageState extends State<MainPage> {
SliverGridDelegateWithFixedCrossAxisCount( SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: min( crossAxisCount: min(
(MediaQuery.sizeOf(context).width ~/ (MediaQuery.sizeOf(context).width ~/
200) 220)
.toInt(), .toInt(),
8), 8),
childAspectRatio: 1.0), childAspectRatio: 0.8,
mainAxisSpacing: 10.0,
crossAxisSpacing: 20.0),
itemCount: itemViewCount, itemCount: itemViewCount,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final product = filteredData[index]; final product = filteredData[index];
return ProductCard( return ProductCard(
imagePath: Image( imagePath: FutureBuilder(
image: future: precacheImage(
Image.network(product.images[0].url) NetworkImage(product.images[0].url),
.image, context),
width: 50, builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
return Image(
image: NetworkImage(
product.images[0].url),
width: 120,
);
} else {
return const CircularProgressIndicator();
}
},
), ),
name: product.title, name: shortString(product.title),
price: product.price.toString(), price: product.price.toStringAsFixed(2),
onTap: () => Navigator.of(context).push( onTap: () => Navigator.of(context).push(
CustomPageRoute( CustomPageRoute(
builder: (context) => DetailPage( builder: (context) => DetailPage(
@@ -253,7 +313,7 @@ class _MainPageState extends State<MainPage> {
); );
}, },
), ),
itemViewCount > 0 itemViewCount > 0 && !isSearching
? Padding( ? Padding(
padding: const EdgeInsets.symmetric(vertical: 10), padding: const EdgeInsets.symmetric(vertical: 10),
child: Center( child: Center(
@@ -269,9 +329,17 @@ class _MainPageState extends State<MainPage> {
Radius.circular(50)), Radius.circular(50)),
), ),
foregroundColor: Colors.white, foregroundColor: Colors.white,
fixedSize: const Size(180, 40),
), ),
child: const Text('Загрузить ещё'), child: const Row(
) mainAxisSize: MainAxisSize.min,
children: [
Text('Загрузить ещё'),
Spacer(),
Icon(Icons.arrow_downward),
Spacer()
],
))
: const CircularProgressIndicator() : const CircularProgressIndicator()
: const Text( : const Text(
'Конец списка', 'Конец списка',

View File

@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:gymlink_module_web/components/app_bar.dart'; import 'package:gymlink_module_web/components/app_bar.dart';
@@ -6,9 +8,11 @@ import 'package:gymlink_module_web/components/order_confirm_item_card.dart';
import 'package:gymlink_module_web/interfaces/items.dart'; import 'package:gymlink_module_web/interfaces/items.dart';
import 'package:gymlink_module_web/pages/order_history.dart'; import 'package:gymlink_module_web/pages/order_history.dart';
import 'package:gymlink_module_web/providers/cart.dart'; import 'package:gymlink_module_web/providers/cart.dart';
import 'package:gymlink_module_web/tools/history.dart';
import 'package:gymlink_module_web/tools/items.dart'; import 'package:gymlink_module_web/tools/items.dart';
import 'package:gymlink_module_web/tools/prefs.dart'; import 'package:gymlink_module_web/tools/prefs.dart';
import 'package:gymlink_module_web/tools/routes.dart'; import 'package:gymlink_module_web/tools/routes.dart';
import 'package:gymlink_module_web/tools/text.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -61,7 +65,63 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
List<GymItem> cartItems = []; List<GymItem> cartItems = [];
double totalPrice = 0; double totalPrice = 0;
List<GymItem> gymCart = []; List<GymItem> gymCart = [];
bool isAgree = false;
bool _isLoading = true; bool _isLoading = true;
final _emailController = TextEditingController();
final _addressController = TextEditingController();
final _nameController = TextEditingController();
Future<void> _addOrderToHistory() async {
String name = _nameController.text;
String email = _emailController.text;
String address = _addressController.text;
Set<String> supplierIdsSet = {};
for (final item in cartItems) {
supplierIdsSet.add(item.supplierId);
}
List<GymHistoryItemDetailProvider> providers = [];
for (final supplierId in supplierIdsSet) {
List<GymItem> items =
cartItems.where((e) => e.supplierId == supplierId).toList();
List<GymHistoryItemDetailItem> detailItems = [];
for (final item in items) {
detailItems.add(GymHistoryItemDetailItem(
id: item.id,
photo: item.images[0].url,
count: item.localCount,
price: item.price.toString(),
name: item.title,
));
}
GymHistoryItemDetailProvider provider = GymHistoryItemDetailProvider(
id: supplierId,
name: items.first.supplierName,
items: detailItems,
// status: 'Не оплачен'
status: Random().nextBool()
? 'Не оплачен'
: Random().nextBool()
? 'Не оплачен'
: Random().nextBool()
? 'Сборка'
: 'Доставляется',
);
providers.add(provider);
}
final order = GymHistoryItemDetail(
id: Random().nextInt(1000000).toString(),
receiver: name,
email: email,
address: address,
sum: totalPrice.toString(),
date: '',
providers: providers,
timestamp: DateTime.now().millisecondsSinceEpoch.toString(),
);
await addToHistory(order);
}
@override @override
void initState() { void initState() {
@@ -85,12 +145,71 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
} }
Future<void> _goToPage() async { Future<void> _goToPage() async {
final Uri url = Uri.parse('https://google.com'); final Uri url = Uri.parse('https://example.org');
if (!await launchUrl(url, webOnlyWindowName: '_blank')) { if (!await launchUrl(url, webOnlyWindowName: '_blank')) {
throw 'Could not launch $url'; throw 'Could not launch $url';
} }
} }
bool _checkInputs() {
final email = _emailController.text;
final address = _addressController.text;
final name = _nameController.text;
if (!RegExp(r"^((?!\.)[\w\-_.]*[^.])(@\w+)(\.\w+(\.\w+)?[^.\W])$")
.hasMatch(email)) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Некорректный email'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('ОК'),
)
],
),
);
return false;
}
if (address.isEmpty) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Адрес не может быть пустым'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('ОК'),
)
],
),
);
return false;
}
if (name.isEmpty) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('ФИО не может быть пустым'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('ОК'),
)
],
),
);
return false;
}
return true;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -107,15 +226,28 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
: ConstrainedBox( : ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 350), constraints: const BoxConstraints(maxHeight: 350),
child: ListView.builder( child: ListView.builder(
shrinkWrap: true,
itemCount: cartItems.length, itemCount: cartItems.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = cartItems[index]; final item = cartItems[index];
return OrderConfirmItemCard( return OrderConfirmItemCard(
name: item.title, name: shortString(item.title),
image: Image( image: FutureBuilder(
future: precacheImage(
NetworkImage(item.images[0].url), context),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
return Image(
image: NetworkImage(item.images[0].url), image: NetworkImage(item.images[0].url),
width: 50, width: 50,
height: 50), height: 50,
);
} else {
return const CircularProgressIndicator();
}
},
),
price: item.price, price: item.price,
count: item.localCount, count: item.localCount,
); );
@@ -129,9 +261,11 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
Expanded( Expanded(
child: Column( child: Column(
children: [ children: [
MarkdownBody(data: '## Итого: $totalPrice'), MarkdownBody(
data: '## Итого: ${totalPrice.toStringAsFixed(2)} руб.'),
Expanded( Expanded(
child: TextField( child: TextField(
controller: _addressController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Адрес доставки', hintText: 'Адрес доставки',
border: OutlineInputBorder( border: OutlineInputBorder(
@@ -142,6 +276,7 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
), ),
Expanded( Expanded(
child: TextField( child: TextField(
controller: _emailController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Электронная почта', hintText: 'Электронная почта',
border: OutlineInputBorder( border: OutlineInputBorder(
@@ -153,6 +288,7 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
), ),
Expanded( Expanded(
child: TextField( child: TextField(
controller: _nameController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Получатель', hintText: 'Получатель',
border: OutlineInputBorder( border: OutlineInputBorder(
@@ -163,8 +299,10 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
), ),
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
if (!_checkInputs()) return;
_goToPage(); _goToPage();
await clearCart(); await clearCart();
await _addOrderToHistory();
Provider.of<CartProvider>(context, listen: false) Provider.of<CartProvider>(context, listen: false)
.updateCartLength(); .updateCartLength();
Navigator.of(context).pushAndRemoveUntil( Navigator.of(context).pushAndRemoveUntil(

View File

@@ -1,10 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gymlink_module_web/components/app_bar.dart'; import 'package:gymlink_module_web/components/app_bar.dart';
import 'package:gymlink_module_web/components/heading.dart'; import 'package:gymlink_module_web/components/heading.dart';
import 'package:gymlink_module_web/components/history_item_card.dart'; import 'package:gymlink_module_web/components/history_item_card.dart';
import 'package:gymlink_module_web/tools/relative.dart'; import 'package:gymlink_module_web/interfaces/items.dart';
import 'package:gymlink_module_web/tools/history.dart';
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart'; import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
List<Map<String, String>> orders = [ List<Map<String, String>> orders = [
@@ -45,32 +47,24 @@ class HistoryPage extends StatefulWidget {
} }
class _HistoryPageState extends State<HistoryPage> { class _HistoryPageState extends State<HistoryPage> {
List<Map<String, String>> my_orders = []; List<GymHistoryItem> my_orders = [];
late Timer _updateTimer; late Timer _updateTimer;
bool _isLoading = true;
bool _isRefreshing = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
my_orders = orders;
ordersRefresh(); ordersRefresh();
} }
void ordersRefresh() { void ordersRefresh() {
_updateTimer = Timer.periodic(const Duration(minutes: 1), _onRefresh); _updateTimer = Timer.periodic(const Duration(minutes: 1), _onRefresh);
Future.microtask(() => _onRefresh(_updateTimer));
} }
void _onLoad() async { void _onLoad() async {
await Future.delayed(const Duration(milliseconds: 1000)); await Future.delayed(const Duration(milliseconds: 1000));
setState(() {
my_orders.add(
{
"image": "product.png",
"price": "120",
"id": "666666",
"date": "11.09.2001"
},
);
});
} }
@override @override
@@ -81,7 +75,12 @@ class _HistoryPageState extends State<HistoryPage> {
Future<void> _onRefresh(Timer timer) async { Future<void> _onRefresh(Timer timer) async {
await Future.delayed(const Duration(milliseconds: 1000)); await Future.delayed(const Duration(milliseconds: 1000));
debugPrint('refreshed'); var orders = await getHistory();
setState(() {
my_orders = orders;
_isLoading = false;
_isRefreshing = false;
});
} }
@override @override
@@ -91,8 +90,47 @@ class _HistoryPageState extends State<HistoryPage> {
body: Column( body: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
const GymLinkHeader(title: 'История заказов'), const GymLinkHeader(
Expanded( title: 'История заказов',
toMain: true,
),
const SizedBox(height: 5),
kIsWeb && !_isLoading
? Center(
child: ElevatedButton(
onPressed: () {
setState(() => _isRefreshing = true);
_onRefresh(_updateTimer);
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(50)),
),
foregroundColor: Colors.white,
fixedSize: const Size(180, 40),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('Обновить'),
Spacer(),
Icon(Icons.refresh),
Spacer()
],
),
),
)
: const SizedBox.shrink(),
const SizedBox(height: 5),
_isRefreshing
? const Center(child: CircularProgressIndicator())
: const SizedBox.shrink(),
const SizedBox(height: 5),
_isLoading
? const Expanded(
child: Center(child: CircularProgressIndicator()))
: Expanded(
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
@@ -102,21 +140,36 @@ class _HistoryPageState extends State<HistoryPage> {
child: RefreshIndicator( child: RefreshIndicator(
edgeOffset: 55, edgeOffset: 55,
onRefresh: () => _onRefresh(_updateTimer), onRefresh: () => _onRefresh(_updateTimer),
child: Stack( child: my_orders.isEmpty
? const Center(child: Text('Нет заказов'))
: Stack(
children: [ children: [
ListView.builder( ListView.builder(
itemCount: my_orders.length, itemCount: my_orders.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = my_orders[index]; final item = my_orders[index];
return HistoryItemCard( return HistoryItemCard(
id: item['id']!, id: item.id,
cost: item['price']!, cost: double.parse(item.sum)
date: item['date']!, .toStringAsFixed(2),
image: Image( date: item.date,
image: AssetImage('assets/${item['image']!}'), image: FutureBuilder(
future: precacheImage(
NetworkImage(item.photo),
context),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
return Image(
image: NetworkImage(
item.photo),
width: 50, width: 50,
);
} else {
return const CircularProgressIndicator();
}
},
), ),
status: OrderStatus.completed,
); );
}, },
), ),
@@ -125,7 +178,9 @@ class _HistoryPageState extends State<HistoryPage> {
), ),
), ),
), ),
getSpacer(context: context) // my_orders.isEmpty
// ? const SizedBox.shrink()
// : getSpacer(context: context)
], ],
), ),
), ),

265
lib/pages/order_info.dart Normal file
View File

@@ -0,0 +1,265 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
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_detail_item_card.dart';
import 'package:gymlink_module_web/interfaces/items.dart';
import 'package:gymlink_module_web/tools/history.dart';
import 'package:gymlink_module_web/tools/text.dart';
import 'package:url_launcher/url_launcher.dart';
final GymHistoryItemDetail item = GymHistoryItemDetail.fromJson({
"id": "12345",
"date": "01.01.1970",
"sum": "45000",
"pay_url": "https://example.org",
"receiver": "Иванов Иван Иванович",
"email": "a@a.ru",
"address": "г. Москва, ул. Пушкина, д. 17",
"providers": [
{
"id": "123",
"name": "Поставщик 1",
"status": "Доставлен",
"items": [
{"photo": "url", "id": "123", "count": 2, "price": "15000"},
{"photo": "url", "id": "123", "count": 2, "price": "15000"}
]
},
{
"id": "123",
"name": "Поставщик 1",
"status": "Доставляется",
"items": [
{"photo": "url", "id": "123", "count": 2, "price": "15000"}
]
}
]
});
class OrderInfoPage extends StatefulWidget {
final String id;
const OrderInfoPage({super.key, required this.id});
@override
State<StatefulWidget> createState() => _OrderInfoPageState();
}
class _OrderInfoPageState extends State<OrderInfoPage> {
GymHistoryItemDetail? detail;
final _scrollController = ScrollController();
late Timer _updateTimer;
bool _isRefreshing = false;
@override
void initState() {
super.initState();
_updateTimer = Timer.periodic(const Duration(minutes: 1), _onRefresh);
_onRefresh(_updateTimer);
}
@override
void dispose() {
_updateTimer.cancel();
super.dispose();
}
Future<void> _onRefresh(Timer timer) async {
return Future.delayed(const Duration(milliseconds: 1000), () async {
var orderInfo = await getHistoryDetail(widget.id);
setState(() {
detail = orderInfo;
_isRefreshing = false;
});
});
}
Future<void> _goToPage() async {
if (detail?.payUrl != null) {
final Uri url = Uri.parse(detail?.payUrl ?? 'https://example.org');
if (!await launchUrl(url, webOnlyWindowName: '_blank')) {
throw 'Could not launch $url';
}
_onRefresh(_updateTimer);
}
}
Widget _buildContent() {
return Column(
children: [
GymLinkHeader(title: "Заказ #${detail?.id} от ${detail?.date}"),
Expanded(
child: RefreshIndicator(
onRefresh: () => _onRefresh(_updateTimer),
edgeOffset: 55,
child: Scrollbar(
controller: _scrollController,
child: ListView(
controller: _scrollController,
children: [
const SizedBox(height: 10),
kIsWeb
? Center(
child: ElevatedButton(
onPressed: () {
setState(() {
_isRefreshing = true;
});
_onRefresh(_updateTimer);
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
shape: const RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(50)),
),
foregroundColor: Colors.white,
fixedSize: const Size(180, 40),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('Обновить'),
Spacer(),
Icon(Icons.refresh),
Spacer()
],
),
),
)
: const SizedBox.shrink(),
const SizedBox(height: 5),
_isRefreshing
? const Center(
child: CircularProgressIndicator(),
)
: const SizedBox.shrink(),
const SizedBox(height: 5),
ListView.builder(
itemCount: detail!.providers.length,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index) {
final provider = detail!.providers[index];
return Padding(
padding: const EdgeInsetsDirectional.symmetric(
vertical: 10,
horizontal: 5,
),
child: Card(
elevation: 3,
child: Column(
children: [
MarkdownBody(data: '# ${provider.name}'),
MarkdownBody(
data: '## Статус: ${provider.status}'),
const MarkdownBody(data: '### Состав:'),
for (final item in provider.items)
OrderDetailCardItemCard(
image: FutureBuilder(
future: precacheImage(
NetworkImage(item.photo), context),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
return Image(
image: NetworkImage(item.photo),
width: 50,
);
} else {
return const CircularProgressIndicator();
}
},
),
name: shortString(item.name),
count: item.count,
price: double.parse(item.price),
),
],
),
),
);
},
),
const SizedBox(
height: 10,
),
SizedBox(
height: 200,
child: Padding(
padding: const EdgeInsetsDirectional.symmetric(
horizontal: 5,
),
child: Card(
elevation: 4,
child: Padding(
padding: const EdgeInsetsDirectional.symmetric(
horizontal: 10, vertical: 20),
child: Column(
// mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
// crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownBody(
data:
'## Итого: ${double.parse(detail!.sum).toStringAsFixed(2)} руб.'),
MarkdownBody(
data:
"### Адрес получателя: __${detail!.address}__"),
MarkdownBody(
data: '### Почта: __${detail!.email}__'),
MarkdownBody(
data: '### ФИО: __${detail!.receiver}__'),
],
),
detail?.payUrl == null
? const SizedBox.shrink()
: Center(
child: ElevatedButton(
onPressed: () async {
await _goToPage();
await payOrder(detail!.id);
},
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).primaryColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(50)),
),
foregroundColor: Colors.white,
fixedSize: const Size(180, 40),
),
child: const Text('Оплатить заказ'),
),
),
],
),
),
),
),
),
],
),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const GymLinkAppBar(),
body: detail == null
? const Center(child: CircularProgressIndicator())
: _buildContent(),
);
}
}

View File

@@ -14,21 +14,14 @@ class GymLinkProvider with ChangeNotifier {
void Function() get onError => _onError; void Function() get onError => _onError;
void onTokenReceived(String token) { void checkToken(String token) {
_token = token; _token = token;
_isLoading = false; _isLoading = false;
notifyListeners(); notifyListeners();
// if (token == 'token123') {
// _isLoading = false;
// notifyListeners();
// } else {
// _isLoading = true;
// notifyListeners();
// }
} }
void changeTheme(int color) { void changeTheme(int color, {bool blackTheme = false}) {
_blackTheme = !_blackTheme; _blackTheme = blackTheme;
_theme = getThemeData(Color(color), _blackTheme); _theme = getThemeData(Color(color), _blackTheme);
notifyListeners(); notifyListeners();
} }

View File

@@ -19,6 +19,9 @@ class MyAppStateMobile extends State<MyApp> {
: MaterialApp( : MaterialApp(
title: 'GymLink Module', title: 'GymLink Module',
theme: theme, theme: theme,
themeMode: context.read<GymLinkProvider>().blackTheme
? ThemeMode.dark
: ThemeMode.light,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
home: const MainPage(), home: const MainPage(),
), ),

View File

@@ -48,13 +48,13 @@ class MyAppStateWeb extends State<MyApp> {
} }
@js.JSExport() @js.JSExport()
void onTokenReceived(String token) { void checkToken(String token) {
context.read<GymLinkProvider>().onTokenReceived(token); context.read<GymLinkProvider>().checkToken(token);
} }
@js.JSExport() @js.JSExport()
void changeColor(int color) { void changeColor(int color, bool blackTheme) {
context.read<GymLinkProvider>().changeTheme(color); context.read<GymLinkProvider>().changeTheme(color, blackTheme: blackTheme);
} }
@js.JSExport() @js.JSExport()

View File

@@ -13,7 +13,7 @@ ThemeData getThemeData(Color color, bool dark) {
).copyWith( ).copyWith(
onPrimary: dark ? materialColor[600] : Colors.white, onPrimary: dark ? materialColor[600] : Colors.white,
), ),
useMaterial3: true, // useMaterial3: true,
); );
} }

99
lib/tools/history.dart Normal file
View File

@@ -0,0 +1,99 @@
import 'dart:convert';
import 'dart:math';
import 'package:gymlink_module_web/interfaces/items.dart';
import 'package:shared_preferences/shared_preferences.dart';
Future<List<GymHistoryItem>> getHistory() async {
final prefs = await SharedPreferences.getInstance();
String historyString = prefs.getString('history') ?? "[]";
List<GymHistoryItem> history = [];
for (var historyItem in jsonDecode(historyString) as List<dynamic>) {
history.add(GymHistoryItem.fromJson(historyItem));
}
history.sort((a, b) => b.timestamp.compareTo(a.timestamp));
return history;
}
Future<void> addToHistory(GymHistoryItemDetail item) async {
final prefs = await SharedPreferences.getInstance();
String historyString = prefs.getString('history') ?? "[]";
List<GymHistoryItem> history = [];
for (var historyItem in jsonDecode(historyString) as List<dynamic>) {
history.add(GymHistoryItem.fromJson(historyItem));
}
item.id = Random().nextInt(100000).toString();
String detailHistoryString = prefs.getString('detail_history') ?? "[]";
List<GymHistoryItemDetail> detailHistory = [];
for (var historyItem in jsonDecode(detailHistoryString) as List<dynamic>) {
detailHistory.add(GymHistoryItemDetail.fromJson(historyItem));
}
List<Map<String, dynamic>> providers = [];
for (final provider in item.providers) {
providers.add(provider.toJson());
}
var json = {
"id": item.id,
"date": DateTime.now()
.toLocal()
.toString()
.split(' ')[0]
.replaceAll('-', '.')
.split('.')
.reversed
.join('.'),
"sum": item.sum,
"pay_url": item.providers.where((e) => e.status == 'Не оплачен').isNotEmpty
? 'https://example.org'
: null,
"receiver": item.receiver,
"email": item.email,
"address": item.address,
"providers": providers,
"timestamp": DateTime.now().millisecondsSinceEpoch.toString(),
};
final detailHistoryItem = GymHistoryItemDetail.fromJson(json);
detailHistory.add(detailHistoryItem);
history.add(GymHistoryItem(
date: detailHistoryItem.date,
id: detailHistoryItem.id,
photo: detailHistoryItem.providers[0].items[0].photo,
sum: detailHistoryItem.sum,
timestamp: detailHistoryItem.timestamp,
));
prefs.setString('history', jsonEncode(history));
prefs.setString('detail_history', jsonEncode(detailHistory));
}
Future<GymHistoryItemDetail?> getHistoryDetail(String id) async {
final prefs = await SharedPreferences.getInstance();
String historyString = prefs.getString('detail_history') ?? "[]";
for (var historyItem in jsonDecode(historyString) as List<dynamic>) {
if (GymHistoryItemDetail.fromJson(historyItem).id == id) {
return GymHistoryItemDetail.fromJson(historyItem);
}
}
return null;
}
Future<void> payOrder(String id) async {
final prefs = await SharedPreferences.getInstance();
String historyString = prefs.getString('detail_history') ?? "[]";
List<GymHistoryItemDetail> history = [];
for (var historyItem in jsonDecode(historyString) as List<dynamic>) {
history.add(GymHistoryItemDetail.fromJson(historyItem));
}
List<GymHistoryItemDetail> newHistory = [];
for (final historyItem in history) {
if (historyItem.id == id) {
for (final provider in historyItem.providers) {
if (provider.status == 'Не оплачен') {
provider.status = 'Оплачен';
}
}
historyItem.payUrl = null;
}
newHistory.add(historyItem);
}
prefs.setString('detail_history', jsonEncode(newHistory));
}

View File

@@ -6,22 +6,29 @@ import 'package:gymlink_module_web/providers/main.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
Future<List<GymItem>> getItems(BuildContext context) async { Future<List<GymItem>> getItems(BuildContext context,
{String searchText = '', String categoryId = ''}) async {
final token = context.read<GymLinkProvider>().token; final token = context.read<GymLinkProvider>().token;
if (token != '') { if (token != '') {
final Uri url = final Uri url = Uri.https('gymlink.freemyip.com', 'api/product/get-list');
Uri.http('gymlink.freemyip.com:8080', 'api/product/get-list');
try { try {
final response = await http.get(url, headers: { final response = await http.post(url,
headers: {
'Authorization': 'Bearer $token', 'Authorization': 'Bearer $token',
}); 'Content-Type': 'application/json'
if (response.statusCode == 200) { },
final data = body: jsonEncode({
jsonDecode(utf8.decode(response.bodyBytes)) as List<dynamic>; "search": searchText,
final items = data.map((e) => GymItem.fromJson(e)).toList(); "page": 0,
"pageSize": 0,
"filter": categoryId,
"direction": 1
}));
if (response.statusCode == 201) {
final items = ItemsDataResponse.fromRawJson(response.body).rows;
return items; return items;
} }
throw Error(); throw response.body;
} catch (e) { } catch (e) {
debugPrint('error: $e'); debugPrint('error: $e');
return await Future.delayed( return await Future.delayed(
@@ -40,7 +47,7 @@ Future<List<GymItem>> getItemsByIds(
return []; return [];
} }
final Uri url = final Uri url =
Uri.http('gymlink.freemyip.com:8080', 'api/product/get-products'); Uri.https('gymlink.freemyip.com', 'api/product/get-products');
try { try {
final response = await http.post(url, final response = await http.post(url,
headers: { headers: {
@@ -64,3 +71,27 @@ Future<List<GymItem>> getItemsByIds(
context.read<GymLinkProvider>().onError(); context.read<GymLinkProvider>().onError();
return []; return [];
} }
Future<List<GymCategory>> getCategories(BuildContext context) async {
final token = context.read<GymLinkProvider>().token;
if (token != '') {
final Uri url = Uri.https(
'gymlink.freemyip.com', 'api/category/get-internal-categories');
try {
final response = await http.get(url, headers: {
'Authorization': 'Bearer $token',
});
if (response.statusCode == 200) {
final categories = CategoryDataResponse.fromRawJson(response.body).rows;
return categories;
}
throw response.body;
} catch (e) {
debugPrint('error: $e');
return await Future.delayed(
const Duration(seconds: 5), () => getCategories(context));
}
}
context.read<GymLinkProvider>().onError();
return [];
}

9
lib/tools/text.dart Normal file
View File

@@ -0,0 +1,9 @@
String shortString(String text, {int length = 10}) {
if (text.length > length) {
String shortText = text.substring(0, length);
return shortText +
(text.substring(0, length + 1).endsWith(' ') ? '' : '...');
} else {
return text;
}
}

View File

@@ -25,6 +25,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
carousel_slider:
dependency: "direct main"
description:
name: carousel_slider
sha256: "9c695cc963bf1d04a47bd6021f68befce8970bcd61d24938e1fb0918cf5d9c42"
url: "https://pub.dev"
source: hosted
version: "4.2.1"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@@ -69,10 +77,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cupertino_icons name: cupertino_icons
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.6" version: "1.0.8"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -118,6 +126,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.1" version: "0.7.1"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2"
url: "https://pub.dev"
source: hosted
version: "2.0.10+1"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -152,6 +168,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.2" version: "4.0.2"
lazy_load_scrollview:
dependency: "direct main"
description:
name: lazy_load_scrollview
sha256: "230c827d6f7ec5e461f0674ef332daae2f78190bf1e4cd84977e51de04b231e3"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@@ -232,6 +256,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.0" version: "1.9.0"
path_parsing:
dependency: transitive
description:
name: path_parsing
sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
url: "https://pub.dev"
source: hosted
version: "1.0.1"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
@@ -256,6 +288,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.1" version: "2.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "6.0.2"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@@ -477,6 +517,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.1" version: "3.1.1"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
url: "https://pub.dev"
source: hosted
version: "1.1.11+1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -517,6 +581,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
sdks: sdks:
dart: ">=3.3.3 <4.0.0" dart: ">=3.3.3 <4.0.0"
flutter: ">=3.19.0" flutter: ">=3.19.0"

View File

@@ -1,6 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<!-- <!--
If you are serving your web app in a path other than the root, change the If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from. href value below to reflect the base path you are serving from.
@@ -14,24 +14,24 @@
This is a placeholder for base href that will be replaced by the value of This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`. the `--base-href` argument provided to `flutter build`.
--> -->
<base href="$FLUTTER_BASE_HREF"> <base href="$FLUTTER_BASE_HREF" />
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta content="IE=Edge" http-equiv="X-UA-Compatible" />
<meta name="description" content="A new Flutter project."> <meta name="description" content="A new Flutter project." />
<!-- iOS meta tags & icons --> <!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-mobile-web-app-title" content="flutter_application_1"> <meta name="apple-mobile-web-app-title" content="flutter_application_1" />
<link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="apple-touch-icon" href="icons/Icon-192.png" />
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/> <link rel="icon" type="image/png" href="favicon.png" />
<title>flutter_application_1</title> <title>flutter_application_1</title>
<link rel='stylesheet' href='css/styles.css'> <link rel="stylesheet" href="css/styles.css" />
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json" />
<!-- <script> <!-- <script>
// The value below is injected by flutter build, do not touch. // The value below is injected by flutter build, do not touch.
@@ -39,18 +39,20 @@
</script> --> </script> -->
<!-- This script adds the flutter initialization JS code --> <!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script> <script src="flutter.js" defer></script>
</head> </head>
<body> <body>
<button id='token'>Token btn</button> <button id="token">Token btn</button>
<button id='colorChangeBtnRed'>Color btn Red</button> <button id="token2">Token btn 2</button>
<button id='colorChangeBtnBlue'>Color btn Blue</button> <button id="colorChangeBtnRed">Color btn Red</button>
<section class='contents'> <button id="colorChangeBtnBlue">Color btn Blue</button>
<button id="clearBtn">Clear</button>
<section class="contents">
<article> <article>
<div id="flutter_target"></div> <div id="flutter_target"></div>
</article> </article>
</section> </section>
<script> <script>
window.addEventListener('load', function(ev) { window.addEventListener('load', function (ev) {
let target = document.querySelector('#flutter_target'); let target = document.querySelector('#flutter_target');
_flutter.loader.loadEntrypoint({ _flutter.loader.loadEntrypoint({
onEntrypointLoaded: async function (engineInitializer) { onEntrypointLoaded: async function (engineInitializer) {
@@ -58,10 +60,10 @@
hostElement: target, hostElement: target,
}); });
await appRunner.runApp(); await appRunner.runApp();
} },
}) });
}); });
</script> </script>
<script src='js/demo-js-interop.js' defer></script> <script src="js/demo-js-interop.js" defer></script>
</body> </body>
</html> </html>

View File

@@ -7,26 +7,22 @@
}; };
let appState = window._appState; let appState = window._appState;
function getToken() { function getToken(token) {
fetch( fetch('https://gymlink.freemyip.com/api/auth/authorize_client', {
'http://gymlink.freemyip.com:8080/api/auth/authorize_client',
{
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
GymKey: 'eeb42dcb-8e5b-4f21-825a-3fc7ada43445', // Just testing token GymKey: token, // Just testing token
id: '123', id: '123',
}), }),
} })
)
.then(res => res.json()) .then(res => res.json())
.catch(e => { .catch(e => {
console.log(e); console.log(e);
setTimeout(getToken, 1000); setTimeout(getToken, 1000);
}) })
.then(data => { .then(data => {
if (data.payload) if (data.payload) appState.checkToken(data.payload.token);
appState.onTokenReceived(data.payload.token);
else { else {
console.log(data); console.log(data);
setTimeout(getToken, 1000); setTimeout(getToken, 1000);
@@ -34,35 +30,39 @@
}); });
} }
let btn = document.getElementById('token'); const btn = document.getElementById('token');
btn.addEventListener('click', getToken); btn.addEventListener('click', () => {
localStorage.clear();
getToken('eeb42dcb-8e5b-4f21-825a-3fc7ada43445');
});
const btn2 = document.getElementById('token2');
btn2.addEventListener('click', () => {
localStorage.clear();
getToken('a8622a61-3142-487e-8db8-b6aebd4f04aa');
});
let colorChangeBtnRed = document.getElementById('colorChangeBtnRed'); let colorChangeBtnRed = document.getElementById('colorChangeBtnRed');
colorChangeBtnRed.addEventListener('click', function () { colorChangeBtnRed.addEventListener('click', function () {
var hexColor = '#FF0000' var hexColor = '#FF0000'.substring(1);
.replace(
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
(m, r, g, b) => '#ff' + r + r + g + g + b + b
)
.substring(1);
var numColor = parseInt(hexColor, 16); var numColor = parseInt(hexColor, 16);
appState.changeColor(numColor); appState.changeColor(numColor, true);
}); });
let colorChangeBtnBlue = document.getElementById('colorChangeBtnBlue'); let colorChangeBtnBlue = document.getElementById('colorChangeBtnBlue');
colorChangeBtnBlue.addEventListener('click', function () { colorChangeBtnBlue.addEventListener('click', function () {
var hexColor = '#0000FF' var hexColor = '#0000FF'.substring(1);
.replace(
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
(m, r, g, b) => '#ff' + r + r + g + g + b + b
)
.substring(1);
var numColor = parseInt(hexColor, 16); var numColor = parseInt(hexColor, 16);
appState.changeColor(numColor); appState.changeColor(numColor, false);
});
let clearBtn = document.getElementById('clearBtn');
clearBtn.addEventListener('click', function () {
localStorage.clear();
window.location.reload();
}); });
function onError() { function onError() {
console.error('aboba'); console.error('Error');
} }
appState.setOnError(onError); appState.setOnError(onError);

View File

@@ -1,51 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Прием платежа с помощью виджета ЮKassa</title>
<!--Подключение библиотеки для инициализации виджета ЮKassa-->
<script src="https://yookassa.ru/checkout-widget/v1/checkout-widget.js"></script>
</head>
<body>
Ниже отобразится платежная форма. Если вы еще не создавали платеж и не передавали токен для инициализации виджета, появится сообщение об ошибке.
<!--Контейнер, в котором будет отображаться платежная форма-->
<div id="payment-form"></div>
Данные банковской карты для оплаты в <b>тестовом магазине</b>:
- номер — <b>5555 5555 5555 4477</b>
- срок действия — <b>01/30</b> (или другая дата, больше текущей)
- CVC — <b>123</b> (или три любые цифры)
- код для прохождения 3-D Secure — <b>123</b> (или три любые цифры)
<a href=https://yookassa.ru/developers/payment-acceptance/testing-and-going-live/testing#test-bank-card>Другие тестовые банковские карты</a>
<script>
//Инициализация виджета. Все параметры обязательные.
const checkout = new window.YooMoneyCheckoutWidget({
confirmation_token: 'ct-287e0c37-000f-5000-8000-16961d35b0fd', //Токен, который перед проведением оплаты нужно получить от ЮKassa
return_url: 'https://example.com/', //Ссылка на страницу завершения оплаты, это может быть любая ваша страница
//При необходимости можно изменить цвета виджета, подробные настройки см. в документации
//customization: {
//Настройка цветовой схемы, минимум один параметр, значения цветов в HEX
//colors: {
//Цвет акцентных элементов: кнопка Заплатить, выбранные переключатели, опции и текстовые поля
//control_primary: '#00BF96', //Значение цвета в HEX
//Цвет платежной формы и ее элементов
//background: '#F2F3F5' //Значение цвета в HEX
//}
//},
error_callback: function(error) {
console.log(error)
}
});
//Отображение платежной формы в контейнере
checkout.render('payment-form');
</script>
</body>
</html>