Compare commits

...

40 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
c8965dab4e Add: Second club page 2024-06-04 12:59:35 +03:00
97664fdb5a Fix: Precaching images 2024-05-31 15:29:35 +03:00
cfa6ef9a67 Fix: index in carousel 2024-05-31 15:28:58 +03:00
7335c55703 Fix: image carousel on web 2024-05-31 15:07:52 +03:00
1eeff4209e Add: images slider 2024-05-31 14:55:24 +03:00
3b593ad733 Fix: Lazy loader on web and its indicator 2024-05-31 14:00:34 +03:00
0438a6feec Add: Getting items by its ids 2024-05-23 22:12:07 +03:00
d6f64465b3 Add: cart items by its ids 2024-05-23 02:04:37 +03:00
e57c7dc0ea Add: detail page from API 2024-05-22 22:14:24 +03:00
e7073cec67 Add: closing module on errors 2024-05-22 15:46:39 +03:00
80da7e9008 Fix: $ to руб 2024-05-22 15:46:24 +03:00
7907dcf6c2 Add: getting products from API 2024-05-22 15:46:09 +03:00
46ba11cd57 Fix: connection problems checks 2024-05-22 00:49:51 +03:00
d8e68f9b34 Add: get token from api 2024-05-22 00:41:44 +03:00
e95fb08e31 Rename: application name 2024-05-22 00:40:48 +03:00
30 changed files with 2235 additions and 622 deletions

View File

@@ -23,7 +23,7 @@ if (flutterVersionName == null) {
}
android {
namespace "com.example.flutter_application_1"
namespace "com.example.gym_app"
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
@@ -42,11 +42,11 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.flutter_application_1"
applicationId "com.example.gym_app"
// 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.
minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}

View File

@@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="flutter_application_1"
android:label="Example Gym App"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
@@ -10,6 +11,7 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:enableOnBackInvokedCallback="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user

View File

@@ -1,4 +1,4 @@
package com.example.flutter_application_1
package com.example.gym_app
import io.flutter.embedding.android.FlutterActivity

View File

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

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/pages/detail.dart';
import 'package:gymlink_module_web/tools/routes.dart';
class BasketItemCard extends StatelessWidget {
final String name;
final String price;
final String id;
final Image image;
final Widget image;
final String quantity;
final VoidCallback onTapPlus;
final VoidCallback onTapMinus;
@@ -32,52 +34,58 @@ class BasketItemCard extends StatelessWidget {
minWidth: 400,
maxWidth: 600,
),
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: [
Text(
name,
style: Theme.of(context).textTheme.bodyLarge,
),
Text('\$$price'),
],
)
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: onTapMinus,
),
const SizedBox(width: 10),
Text(quantity),
const SizedBox(width: 10),
IconButton(
icon: const Icon(Icons.add),
onPressed: onTapPlus,
),
],
)
],
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
CustomPageRoute(builder: (context) => DetailPage(id: id)));
},
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: [
Text(
name,
style: Theme.of(context).textTheme.bodyLarge,
),
Text('$price руб.'),
],
)
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: onTapMinus,
),
const SizedBox(width: 10),
Text(quantity),
const SizedBox(width: 10),
IconButton(
icon: const Icon(Icons.add),
onPressed: onTapPlus,
),
],
)
],
),
),
),
),

View File

@@ -1,8 +1,11 @@
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 {
final String title;
const GymLinkHeader({super.key, required this.title});
final bool toMain;
const GymLinkHeader({super.key, required this.title, this.toMain = false});
@override
Widget build(BuildContext context) {
@@ -13,7 +16,13 @@ class GymLinkHeader extends StatelessWidget {
Row(
children: [
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)),
Text(title, style: Theme.of(context).textTheme.titleLarge),
],

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.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 }
@@ -14,15 +16,13 @@ class HistoryItemCard extends StatelessWidget {
final String id;
final String cost;
final String date;
final Image image;
final OrderStatus status;
final Widget image;
const HistoryItemCard({
super.key,
required this.id,
required this.cost,
required this.date,
required this.status,
required this.image,
});
@@ -38,38 +38,42 @@ class HistoryItemCard extends StatelessWidget {
minWidth: 600,
maxWidth: 800,
),
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.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
image,
const SizedBox(width: 20),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownBody(
data: '### Заказ **№$id** от $date',
),
MarkdownBody(
data: 'Статус: **${orderStatusMap[status]}**'),
MarkdownBody(data: 'Сумма: **\$$cost**'),
],
)
],
),
],
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
CustomPageRoute(builder: (context) => OrderInfoPage(id: id)));
},
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.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
image,
const SizedBox(width: 20),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownBody(
data: '### Заказ **№$id** от $date',
),
MarkdownBody(data: 'Сумма: **$cost руб.**'),
],
)
],
),
],
),
),
),
),

View File

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

View File

@@ -5,7 +5,7 @@ class OrderConfirmItemCard extends StatelessWidget {
final String name;
final int count;
final double price;
final Image image;
final Widget image;
const OrderConfirmItemCard({
super.key,
@@ -46,7 +46,8 @@ class OrderConfirmItemCard extends StatelessWidget {
name,
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')
],
),
),
),
),
);
}
}

325
lib/interfaces/items.dart Normal file
View File

@@ -0,0 +1,325 @@
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 {
final String id;
final String externalId;
final String title;
final String description;
final int count;
final double price;
final String categoryId;
final List<GymImage> images;
final String supplierId;
final String supplierName;
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,
required this.supplierId,
required this.supplierName,
});
factory GymItem.fromRawJson(String str) => GymItem.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory GymItem.fromJson(Map<String, dynamic> json) {
return 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<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() => {
"id": id,
"ExternalId": externalId,
"title": title,
"description": description,
"count": count,
"price": price,
"categoryId": categoryId,
"images": List<dynamic>.from(images.map((x) => x.toJson())),
"supplier":
supplierId == '' ? null : {"id": supplierId, "title": supplierName},
};
}
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<String, dynamic> json) => GymImage(
id: json["id"],
deletedAt: json["deletedAt"],
url: json["url"],
);
Map<String, dynamic> toJson() => {
"id": id,
"deletedAt": deletedAt,
"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

@@ -1,19 +1,40 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:gymlink_module_web/main_mobile.dart';
import 'package:gymlink_module_web/providers/main.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
runApp(const MyExampleApp());
}
Future<String> getToken(String token, String clientId) async {
var url = Uri.https('gymlink.freemyip.com', 'api/auth/authorize_client');
try {
var response = await http.post(url,
body: {'GymKey': token, 'id': clientId}); // Just testing token
var decodedBody = jsonDecode(response.body) as Map;
if (decodedBody['payload'] == null) {
return '';
}
return decodedBody['payload']['token'];
} catch (e) {
return '';
}
}
class MyExampleApp extends StatelessWidget {
const MyExampleApp({super.key});
@override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
return MaterialApp(
title: 'GymLink Example App',
debugShowCheckedModeBanner: false,
@@ -35,18 +56,49 @@ Widget getDrawer(BuildContext context) => Drawer(
children: [
const DrawerHeader(child: Text('Drawer Header')),
ListTile(
leading: const Icon(Icons.home),
title: const Text('Home'),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const ExampleMainPage(),
),
),
),
leading: const Icon(Icons.home),
title: const Text('Home'),
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(
builder: (context) => const ExampleMainPage(),
),
);
}),
ListTile(
leading: const Icon(Icons.sell),
title: const Text('Club 2'),
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(
builder: (context) => ChangeNotifierProvider(
create: (_) => GymLinkProvider(),
child: Consumer<GymLinkProvider>(
builder: (_, value, __) => const ExampleClub2Page(),
),
),
),
);
}),
ListTile(
leading: const Icon(Icons.search),
title: const Text('Example page'),
onTap: () => Navigator.of(context).push(MaterialPageRoute(
onTap: () =>
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => const ExampleSecondPage(),
)),
),
@@ -71,28 +123,54 @@ class _ExamplePageState extends State<ExamplePage> {
@override
void initState() {
super.initState();
Future.microtask(
() => context.read<GymLinkProvider>().onTokenReceived('token123'));
// Future.microtask(
// () => context.read<GymLinkProvider>().onTokenReceived('token123'));
Future.microtask(() => context
.read<GymLinkProvider>()
.setTheme(ThemeData.light(useMaterial3: true)));
Future.microtask(() => context.read<GymLinkProvider>().setOnError(() {
const snackBar = SnackBar(
content: Text('Ошибка подключения'),
duration: Duration(seconds: 3), // Длительность отображения Snackbar
behavior: SnackBarBehavior
.fixed, // Поведение Snackbar (fixed или floating)
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Future.delayed(const Duration(seconds: 3))
.then((value) => _setToken());
}));
Future.microtask(() async {
await _setToken();
});
}
Future<void> _setToken() async {
final token = await getToken('eeb42dcb-8e5b-4f21-825a-3fc7ada43445', '123');
if (token != '') {
context.read<GymLinkProvider>().checkToken(token);
} else {
context.read<GymLinkProvider>().onError();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('GymLink Example App'),
title: const Text('GymLink Example App Gym 1'),
),
resizeToAvoidBottomInset: false,
drawer: getDrawer(context),
body: Column(
children: [
const Text('test'),
IconButton(
icon: const Icon(Icons.abc),
icon: const Icon(Icons.colorize),
onPressed: () {
context.read<GymLinkProvider>().onTokenReceived('token123');
context.read<GymLinkProvider>().changeTheme(
Random().nextInt(0xffffff + 1),
blackTheme: Random().nextBool());
},
),
const Expanded(
@@ -104,13 +182,73 @@ class _ExamplePageState extends State<ExamplePage> {
const Text('Bottom text')
],
),
floatingActionButton: IconButton(
icon: const Icon(Icons.search),
onPressed: () {
context
.read<GymLinkProvider>()
.changeTheme(Random().nextInt(0xffffff + 1));
},
);
}
}
class ExampleClub2Page extends StatefulWidget {
const ExampleClub2Page({super.key});
@override
State<ExampleClub2Page> createState() => _ExampleClub2PageState();
}
class _ExampleClub2PageState extends State<ExampleClub2Page> {
@override
void initState() {
super.initState();
// Future.microtask(
// () => context.read<GymLinkProvider>().onTokenReceived('token123'));
Future.microtask(() => context
.read<GymLinkProvider>()
.setTheme(ThemeData.light(useMaterial3: true)));
Future.microtask(() => context.read<GymLinkProvider>().setOnError(() {
const snackBar = SnackBar(
content: Text('Ошибка подключения'),
duration: Duration(seconds: 3), // Длительность отображения Snackbar
behavior: SnackBarBehavior
.fixed, // Поведение Snackbar (fixed или floating)
);
ScaffoldMessenger.of(context).showSnackBar(snackBar);
Future.delayed(const Duration(seconds: 3))
.then((value) => _setToken());
}));
Future.microtask(() async {
await _setToken();
});
}
Future<void> _setToken() async {
final token = await getToken('a8622a61-3142-487e-8db8-b6aebd4f04aa', '123');
context.read<GymLinkProvider>().changeTheme(0xFFAABCAB);
if (token != '') {
context.read<GymLinkProvider>().checkToken(token);
} else {
context.read<GymLinkProvider>().onError();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('GymLink Example App Gym2'),
),
resizeToAvoidBottomInset: false,
drawer: getDrawer(context),
body: const Column(
children: [
Text('test'),
Expanded(
child: MyApp(),
),
SizedBox(
height: 20,
),
Text('Bottom text')
],
),
);
}
@@ -126,8 +264,16 @@ class ExampleSecondPage extends StatelessWidget {
title: const Text('GymLink Example App'),
),
drawer: getDrawer(context),
body: const Center(
child: Text('Example page'),
body: Center(
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

@@ -2,11 +2,14 @@ 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/main.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:gymlink_module_web/tools/text.dart';
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
import 'package:provider/provider.dart';
@@ -56,62 +59,71 @@ class BasketPage extends StatefulWidget {
}
class _BasketPageState extends State<BasketPage> {
List<Map<String, dynamic>> cartItems = [];
int totalPrice = 0;
List<GymItem> cartItems = [];
double totalPrice = 0;
List<GymItem> gymCart = [];
bool _isLoading = true;
@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 itemIds =
value.map((element) => element['id'] as String).toList();
final items = await getItemsByIds(context, itemIds);
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);
_isLoading = false;
});
}));
}
void _updateCart() {
Provider.of<CartProvider>(context, listen: false).updateCartLength();
Provider.of<GymLinkProvider>(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);
}
}
void addItem(String id) async {
final item =
cartItems.firstWhere((element) => element.id == id, orElse: () {
final cartItem = gymCart.firstWhere((element) => element.id == id);
cartItem.localCount = 0;
return cartItem;
});
if (item.localCount + 1 > item.count) {
return;
}
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);
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 +154,9 @@ class _BasketPageState extends State<BasketPage> {
onPressed: () {
removeItemFromCart(id);
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) {
_updateCart();
@@ -224,76 +238,24 @@ class _BasketPageState extends State<BasketPage> {
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),
_isLoading
? 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.pushAndRemoveUntil(
context,
CustomPageRoute(
builder: (_) => const MainPage()),
(route) => route.isFirst),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
shape: const RoundedRectangleBorder(
@@ -302,27 +264,102 @@ class _BasketPageState extends State<BasketPage> {
),
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: shortString(item.title),
price: item.price.toStringAsFixed(2),
id: item.id,
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),
width: 50,
);
} else {
return const CircularProgressIndicator();
}
},
),
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.toStringAsFixed(2)} руб.',
),
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),
],
),
),
],
),
);

View File

@@ -1,24 +1,27 @@
import 'dart:convert';
import 'dart:math';
import 'package:carousel_slider/carousel_slider.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/interfaces/items.dart';
import 'package:gymlink_module_web/pages/basket.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:gymlink_module_web/tools/text.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
//TODO: Сделать получение инфы через объект
class DetailPage extends StatefulWidget {
final String name;
final String description;
final String price;
final String id;
final Image image;
const DetailPage({
super.key,
required this.name,
required this.description,
required this.price,
required this.id,
required this.image,
});
@override
@@ -28,10 +31,13 @@ class DetailPage extends StatefulWidget {
class _DetailPageState extends State<DetailPage> {
bool isInCart = false;
int quantity = 0;
GymItem? item;
String? categoryName;
final CarouselController _carouselController = CarouselController();
int _currentImage = 0;
@override
void initState() {
super.initState();
getCart().then((value) {
setState(() {
isInCart = value.any((element) => element['id'] == widget.id);
@@ -41,6 +47,40 @@ class _DetailPageState extends State<DetailPage> {
}
});
});
_getItem();
super.initState();
}
Future<void> _getItem() async {
final Uri url =
Uri.https('gymlink.freemyip.com', 'api/product/get/${widget.id}');
final response = await http.get(url, headers: {
'Authorization': 'Bearer ${context.read<GymLinkProvider>().token}',
});
if (response.statusCode == 200) {
final data =
GymItem.fromJson(jsonDecode(utf8.decode(response.bodyBytes)));
setState(() {
item = data;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
for (var element in item!.images) {
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;
});
});
}
}
}
Widget _buildRowOrCol(
@@ -48,12 +88,6 @@ class _DetailPageState extends State<DetailPage> {
required BuildContext context,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.spaceAround,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center}) {
// if (false && MediaQuery.of(context).size.width > 600) {
// return Row(
// mainAxisAlignment: mainAxisAlignment,
// crossAxisAlignment: crossAxisAlignment,
// children: children);
// }
return Column(
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
@@ -83,117 +117,245 @@ class _DetailPageState extends State<DetailPage> {
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<CartProvider>().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<CartProvider>().updateCartLength();
}
},
),
const SizedBox(width: 10),
Text('$quantity'),
const SizedBox(width: 10),
IconButton(
icon: const Icon(Icons.add),
onPressed: () async {
if (item!.count > quantity) {
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('Открыть корзину'),
),
)
],
);
}
}
double _getAspectRatio() {
double width = MediaQuery.sizeOf(context).width;
double height = MediaQuery.sizeOf(context).height;
return max(width, height) / min(width, height);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const GymLinkAppBar(),
body: Column(mainAxisAlignment: MainAxisAlignment.start, children: [
GymLinkHeader(title: widget.name),
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20),
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
// height: MediaQuery.sizeOf(context).height,
child: _buildRowOrCol(
context: context,
mainAxisAlignment: MainAxisAlignment.spaceAround,
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),
body: item != null
? Column(mainAxisAlignment: MainAxisAlignment.start, children: [
GymLinkHeader(title: shortString(item!.title, length: 20)),
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20),
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
child: _buildRowOrCol(
context: context,
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
item!.images.length > 1
? Column(children: [
CarouselSlider.builder(
itemCount: item!.images.length,
itemBuilder: (context, index, realIdx) {
return Center(
child: Image.network(
item!.images[index].url,
width: min(
550,
MediaQuery.sizeOf(context)
.width)),
);
},
carouselController: _carouselController,
options: CarouselOptions(
enlargeCenterPage: true,
height: min(
MediaQuery.sizeOf(context).height -
100,
400),
enableInfiniteScroll: false,
onPageChanged: (index, reason) {
setState(() {
_currentImage = index;
});
}),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: item!.images
.asMap()
.entries
.map((entry) {
return GestureDetector(
onTap: () => _carouselController
.animateToPage(entry.key),
child: Container(
width: 12.0,
height: 12.0,
margin: const EdgeInsets.symmetric(
vertical: 8.0, horizontal: 4.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: (Theme.of(context)
.brightness ==
Brightness.dark
? Colors.white
: Colors.black)
.withOpacity(
_currentImage == entry.key
? 0.9
: 0.4)),
),
);
}).toList(),
),
])
: Image.network(
item!.images[0].url,
height: 400,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Text(
item!.title,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
),
child: SingleChildScrollView(
Center(
child: Padding(
padding: const EdgeInsetsDirectional.all(15),
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 100,
),
child: Text(
widget.description,
style: Theme.of(context).textTheme.bodyMedium,
),
padding: const EdgeInsets.symmetric(vertical: 10),
child: Chip(
label: Text(categoryName != null
? (categoryName == ""
? "Без категории"
: categoryName!)
: ''),
backgroundColor: Colors.white,
labelStyle:
const TextStyle(color: Colors.black),
),
),
),
),
),
),
Align(
alignment: const AlignmentDirectional(0, -1),
child: Padding(
padding:
const EdgeInsetsDirectional.fromSTEB(0, 30, 0, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Стоимость ${widget.price}',
style: Theme.of(context).textTheme.bodyLarge,
Center(
child: MarkdownBody(
data: '### Остаток: _${item!.count}_',
)),
item!.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: SingleChildScrollView(
child: Padding(
padding:
const EdgeInsetsDirectional.all(
15),
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 100,
),
child: Text(
item!.description,
style: Theme.of(context)
.textTheme
.bodyMedium,
),
),
),
),
),
),
)
: const SizedBox.shrink(),
Align(
alignment: const AlignmentDirectional(0, -1),
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(
0, 30, 0, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Стоимость ${item!.price.toStringAsFixed(2)}руб.',
style:
Theme.of(context).textTheme.bodyLarge,
),
_buildButton()
],
),
),
_buildButton()
],
),
),
],
),
),
],
),
),
),
])
: const Center(
child: CircularProgressIndicator(),
),
),
),
]),
);
}
}

View File

@@ -3,17 +3,17 @@ 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';
import 'package:gymlink_module_web/tools/text.dart';
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
const List<Map<String, String>> testData = [
{
@@ -65,48 +65,74 @@ class MainPage extends StatefulWidget {
class _MainPageState extends State<MainPage> {
String searchText = '';
List<Map<String, String>> filteredData = [];
List<GymItem> filteredData = [];
int cartLength = 0;
int itemViewCount = 0;
bool isLoading = false;
bool isSearching = false;
List<GymCategory> categories = [];
GymCategory? selectedCategory;
final ScrollController _scrollController = ScrollController();
final TextEditingController _searchField = TextEditingController();
@override
void initState() {
super.initState();
filteredData = testData;
getCart().then((value) {
setState(() {
cartLength = value.length;
});
}).whenComplete(() {
if (mounted) {
setState(() {});
}
});
}
Future<void> _goToPage() async {
final Uri url = Uri.parse('https://google.com');
if (!await launchUrl(url, webOnlyWindowName: '_blank')) {
throw 'Could not launch $url';
}
getCategories(context).then((value) => setState(() {
categories = value;
_onSearch();
}));
}
void _onLoad() async {
await Future.delayed(const Duration(milliseconds: 1000));
debugPrint('aye');
if (itemViewCount < filteredData.length) {
setState(() {
isLoading = true;
});
await Future.delayed(const Duration(seconds: 1));
setState(() {
itemViewCount = min(filteredData.length, itemViewCount + 5);
isLoading = false;
});
}
}
void _searchItems({String searchText = '', String categoryId = ''}) async {
setState(() {
isSearching = true;
});
final data =
await getItems(context, searchText: searchText, categoryId: categoryId);
setState(() {
filteredData = data;
itemViewCount = min(filteredData.length, 5);
});
WidgetsBinding.instance.addPostFrameCallback((_) {
for (var element in filteredData.sublist(0, itemViewCount)) {
precacheImage(NetworkImage(element.images[0].url), context);
}
});
setState(() {
isSearching = false;
});
}
void _onSearch() {
final categoryId = selectedCategory == null ? '' : selectedCategory!.id;
setState(() {
filteredData = testData
.where((element) => (element['name']!).contains(searchText))
.toList();
searchText = _searchField.text.trim().toLowerCase();
});
_searchItems(searchText: searchText, categoryId: categoryId);
}
@override
Widget build(BuildContext context) {
final cartL = context.watch<CartProvider>().cartLength;
final onError = context.read<GymLinkProvider>().onError;
return Scaffold(
appBar: const GymLinkAppBar(),
body: Column(
@@ -119,19 +145,22 @@ class _MainPageState extends State<MainPage> {
children: [
Expanded(
child: TextField(
onChanged: (value) => setState(() {
searchText = value;
onChanged: (value) {
searchText = value.trim().toLowerCase();
if (searchText == '') {
_onSearch();
}
}),
},
controller: _searchField,
textInputAction: TextInputAction.search,
onSubmitted: (_) => _onSearch(),
decoration: InputDecoration(
hintText: 'Поиск',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(50),
),
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 8),
padding: const EdgeInsets.only(right: 5),
child: ElevatedButton(
onPressed: _onSearch,
style: ElevatedButton.styleFrom(
@@ -154,13 +183,12 @@ class _MainPageState extends State<MainPage> {
),
),
),
getSpacer(context: context, flex: 2),
// getSpacer(context: context, flex: 2),
const SizedBox(
width: 8,
),
ElevatedButton(
onPressed: () {
onError();
Navigator.of(context).push(CustomPageRoute(
builder: (context) => const HistoryPage(),
));
@@ -188,46 +216,142 @@ 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(
child: LazyLoadScrollView(
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),
isLoading: isLoading,
child: Scrollbar(
controller: _scrollController,
child: ListView(
controller: _scrollController,
children: [
filteredData.isEmpty &&
(searchText != '' || selectedCategory != null) &&
!isSearching
? const Center(child: Text('Ничего не найдено'))
: isSearching
? const Center(child: CircularProgressIndicator())
: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: min(
(MediaQuery.sizeOf(context).width ~/
220)
.toInt(),
8),
childAspectRatio: 0.8,
mainAxisSpacing: 10.0,
crossAxisSpacing: 20.0),
itemCount: itemViewCount,
itemBuilder: (context, index) {
final product = filteredData[index];
return ProductCard(
imagePath: FutureBuilder(
future: precacheImage(
NetworkImage(product.images[0].url),
context),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
return Image(
image: NetworkImage(
product.images[0].url),
width: 120,
);
} else {
return const CircularProgressIndicator();
}
},
),
name: shortString(product.title),
price: product.price.toStringAsFixed(2),
onTap: () => Navigator.of(context).push(
CustomPageRoute(
builder: (context) => DetailPage(
id: product.id,
),
),
),
);
},
),
itemViewCount > 0 && !isSearching
? Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Center(
child: itemViewCount < filteredData.length
? !isLoading
? ElevatedButton(
onPressed: _onLoad,
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.arrow_downward),
Spacer()
],
))
: const CircularProgressIndicator()
: const Text(
'Конец списка',
style: TextStyle(
fontSize: 10,
color: Color(0x88000000)),
),
),
),
),
);
},
),
],
)
: const SizedBox.shrink(),
],
),
),
),
),

View File

@@ -1,12 +1,18 @@
import 'dart:math';
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_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/history.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:gymlink_module_web/tools/text.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -56,33 +62,154 @@ class OrderConfirmationPage extends StatefulWidget {
}
class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
List<Map<String, dynamic>> cartItems = [];
int totalPrice = 0;
List<GymItem> cartItems = [];
double totalPrice = 0;
List<GymItem> gymCart = [];
bool isAgree = false;
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
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 itemIds =
value.map((element) => element['id'] as String).toList();
final items = await getItemsByIds(context, itemIds);
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);
_isLoading = false;
});
}));
}
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')) {
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
Widget build(BuildContext context) {
return Scaffold(
@@ -94,24 +221,39 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
const GymLinkHeader(title: 'Оформление заказа'),
const MarkdownBody(data: '## Состав заказа:'),
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 350),
child: ListView.builder(
itemCount: cartItems.length,
itemBuilder: (context, index) {
final item = cartItems[index];
return OrderConfirmItemCard(
name: item['name'],
image: Image(
image: AssetImage('assets/${item['image']}'),
width: 50,
height: 50),
price: double.parse(item['price']),
count: item['count'],
);
},
),
),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 350),
child: ListView.builder(
shrinkWrap: true,
itemCount: cartItems.length,
itemBuilder: (context, index) {
final item = cartItems[index];
return OrderConfirmItemCard(
name: shortString(item.title),
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),
width: 50,
height: 50,
);
} else {
return const CircularProgressIndicator();
}
},
),
price: item.price,
count: item.localCount,
);
},
),
),
),
const SizedBox(
height: 10,
@@ -119,9 +261,11 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
Expanded(
child: Column(
children: [
MarkdownBody(data: '## Итого: $totalPrice'),
MarkdownBody(
data: '## Итого: ${totalPrice.toStringAsFixed(2)} руб.'),
Expanded(
child: TextField(
controller: _addressController,
decoration: InputDecoration(
hintText: 'Адрес доставки',
border: OutlineInputBorder(
@@ -132,6 +276,7 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
),
Expanded(
child: TextField(
controller: _emailController,
decoration: InputDecoration(
hintText: 'Электронная почта',
border: OutlineInputBorder(
@@ -143,6 +288,7 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
),
Expanded(
child: TextField(
controller: _nameController,
decoration: InputDecoration(
hintText: 'Получатель',
border: OutlineInputBorder(
@@ -153,8 +299,10 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
),
ElevatedButton(
onPressed: () async {
if (!_checkInputs()) return;
_goToPage();
await clearCart();
await _addOrderToHistory();
Provider.of<CartProvider>(context, listen: false)
.updateCartLength();
Navigator.of(context).pushAndRemoveUntil(

View File

@@ -1,10 +1,12 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
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/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';
List<Map<String, String>> orders = [
@@ -45,32 +47,24 @@ class HistoryPage extends StatefulWidget {
}
class _HistoryPageState extends State<HistoryPage> {
List<Map<String, String>> my_orders = [];
List<GymHistoryItem> my_orders = [];
late Timer _updateTimer;
bool _isLoading = true;
bool _isRefreshing = false;
@override
void initState() {
super.initState();
my_orders = orders;
ordersRefresh();
}
void ordersRefresh() {
_updateTimer = Timer.periodic(const Duration(minutes: 1), _onRefresh);
Future.microtask(() => _onRefresh(_updateTimer));
}
void _onLoad() async {
await Future.delayed(const Duration(milliseconds: 1000));
setState(() {
my_orders.add(
{
"image": "product.png",
"price": "120",
"id": "666666",
"date": "11.09.2001"
},
);
});
}
@override
@@ -81,7 +75,12 @@ class _HistoryPageState extends State<HistoryPage> {
Future<void> _onRefresh(Timer timer) async {
await Future.delayed(const Duration(milliseconds: 1000));
debugPrint('refreshed');
var orders = await getHistory();
setState(() {
my_orders = orders;
_isLoading = false;
_isRefreshing = false;
});
}
@override
@@ -91,44 +90,100 @@ class _HistoryPageState extends State<HistoryPage> {
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const GymLinkHeader(title: 'История заказов'),
Expanded(
child: Row(
children: [
Expanded(
child: LazyLoadScrollView(
onEndOfPage: _onLoad,
scrollOffset: 200,
child: RefreshIndicator(
edgeOffset: 55,
onRefresh: () => _onRefresh(_updateTimer),
child: Stack(
children: [
ListView.builder(
itemCount: my_orders.length,
itemBuilder: (context, index) {
final item = my_orders[index];
return HistoryItemCard(
id: item['id']!,
cost: item['price']!,
date: item['date']!,
image: Image(
image: AssetImage('assets/${item['image']!}'),
width: 50,
),
status: OrderStatus.completed,
);
},
),
],
const GymLinkHeader(
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(
children: [
Expanded(
child: LazyLoadScrollView(
onEndOfPage: _onLoad,
scrollOffset: 200,
child: RefreshIndicator(
edgeOffset: 55,
onRefresh: () => _onRefresh(_updateTimer),
child: my_orders.isEmpty
? const Center(child: Text('Нет заказов'))
: Stack(
children: [
ListView.builder(
itemCount: my_orders.length,
itemBuilder: (context, index) {
final item = my_orders[index];
return HistoryItemCard(
id: item.id,
cost: double.parse(item.sum)
.toStringAsFixed(2),
date: item.date,
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();
}
},
),
);
},
),
],
),
),
),
),
// my_orders.isEmpty
// ? const SizedBox.shrink()
// : getSpacer(context: context)
],
),
),
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

@@ -6,24 +6,22 @@ class GymLinkProvider with ChangeNotifier {
bool get isLoading => _isLoading;
bool _blackTheme = false;
bool get blackTheme => _blackTheme;
String _token = '';
String get token => _token;
ThemeData _theme = myTheme;
ThemeData get theme => _theme;
void Function() _onError = () => {};
void Function() get onError => _onError;
void onTokenReceived(String token) {
if (token == 'token123') {
_isLoading = false;
notifyListeners();
} else {
_isLoading = true;
notifyListeners();
}
void checkToken(String token) {
_token = token;
_isLoading = false;
notifyListeners();
}
void changeTheme(int color) {
_blackTheme = !_blackTheme;
void changeTheme(int color, {bool blackTheme = false}) {
_blackTheme = blackTheme;
_theme = getThemeData(Color(color), _blackTheme);
notifyListeners();
}
@@ -34,6 +32,11 @@ class GymLinkProvider with ChangeNotifier {
}
void setOnError(void Function() onError) {
_onError = onError;
_onError = () {
_token = '';
_isLoading = true;
onError();
notifyListeners();
};
}
}

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ ThemeData getThemeData(Color color, bool dark) {
).copyWith(
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));
}

97
lib/tools/items.dart Normal file
View File

@@ -0,0 +1,97 @@
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<List<GymItem>> getItems(BuildContext context,
{String searchText = '', String categoryId = ''}) async {
final token = context.read<GymLinkProvider>().token;
if (token != '') {
final Uri url = Uri.https('gymlink.freemyip.com', 'api/product/get-list');
try {
final response = await http.post(url,
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json'
},
body: jsonEncode({
"search": searchText,
"page": 0,
"pageSize": 0,
"filter": categoryId,
"direction": 1
}));
if (response.statusCode == 201) {
final items = ItemsDataResponse.fromRawJson(response.body).rows;
return items;
}
throw response.body;
} catch (e) {
debugPrint('error: $e');
return await Future.delayed(
const Duration(seconds: 5), () => getItems(context));
}
}
context.read<GymLinkProvider>().onError();
return [];
}
Future<List<GymItem>> getItemsByIds(
BuildContext context, List<String> ids) async {
final token = context.read<GymLinkProvider>().token;
if (token != '') {
if (ids.isEmpty) {
return [];
}
final Uri url =
Uri.https('gymlink.freemyip.com', 'api/product/get-products');
try {
final response = await http.post(url,
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json'
},
body: jsonEncode({"ids": ids}));
if (response.statusCode == 201) {
final data =
jsonDecode(utf8.decode(response.bodyBytes)) as List<dynamic>;
final items = data.map((e) => GymItem.fromJson(e)).toList();
return items;
}
throw response.body;
} catch (e) {
debugPrint('error: $e');
return await Future.delayed(
const Duration(seconds: 5), () => getItemsByIds(context, ids));
}
}
context.read<GymLinkProvider>().onError();
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"
source: hosted
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:
dependency: transitive
description:
@@ -69,10 +77,10 @@ packages:
dependency: "direct main"
description:
name: cupertino_icons
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
source: hosted
version: "1.0.6"
version: "1.0.8"
fake_async:
dependency: transitive
description:
@@ -118,6 +126,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: "direct dev"
description: flutter
@@ -152,6 +168,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -232,6 +256,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -256,6 +288,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.1"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "6.0.2"
platform:
dependency: transitive
description:
@@ -477,6 +517,30 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@@ -517,6 +581,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.3.3 <4.0.0"
flutter: ">=3.19.0"

View File

@@ -40,6 +40,7 @@ dependencies:
lazy_load_scrollview: ^1.3.0
cupertino_icons: ^1.0.8
flutter_svg: ^2.0.10+1
carousel_slider: ^4.2.1
dev_dependencies:
flutter_test:

View File

@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<!--
<head>
<!--
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.
@@ -14,54 +14,56 @@
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<base href="$FLUTTER_BASE_HREF" />
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<meta charset="UTF-8" />
<meta content="IE=Edge" http-equiv="X-UA-Compatible" />
<meta name="description" content="A new Flutter project." />
<!-- iOS meta tags & icons -->
<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-title" content="flutter_application_1">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- iOS meta tags & icons -->
<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-title" content="flutter_application_1" />
<link rel="apple-touch-icon" href="icons/Icon-192.png" />
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png" />
<title>flutter_application_1</title>
<link rel='stylesheet' href='css/styles.css'>
<link rel="manifest" href="manifest.json">
<title>flutter_application_1</title>
<link rel="stylesheet" href="css/styles.css" />
<link rel="manifest" href="manifest.json" />
<!-- <script>
<!-- <script>
// The value below is injected by flutter build, do not touch.
const serviceWorkerVersion = null;
</script> -->
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<button id='token'>Token btn</button>
<button id='colorChangeBtnRed'>Color btn Red</button>
<button id='colorChangeBtnBlue'>Color btn Blue</button>
<section class='contents'>
<article>
<div id="flutter_target"></div>
</article>
</section>
<script>
window.addEventListener('load', function(ev) {
let target = document.querySelector('#flutter_target');
_flutter.loader.loadEntrypoint({
onEntrypointLoaded: async function (engineInitializer) {
let appRunner = await engineInitializer.initializeEngine({
hostElement: target,
});
await appRunner.runApp();
}
})
});
</script>
<script src='js/demo-js-interop.js' defer></script>
</body>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<button id="token">Token btn</button>
<button id="token2">Token btn 2</button>
<button id="colorChangeBtnRed">Color btn Red</button>
<button id="colorChangeBtnBlue">Color btn Blue</button>
<button id="clearBtn">Clear</button>
<section class="contents">
<article>
<div id="flutter_target"></div>
</article>
</section>
<script>
window.addEventListener('load', function (ev) {
let target = document.querySelector('#flutter_target');
_flutter.loader.loadEntrypoint({
onEntrypointLoaded: async function (engineInitializer) {
let appRunner = await engineInitializer.initializeEngine({
hostElement: target,
});
await appRunner.runApp();
},
});
});
</script>
<script src="js/demo-js-interop.js" defer></script>
</body>
</html>

View File

@@ -7,37 +7,62 @@
};
let appState = window._appState;
let btn = document.getElementById('token');
btn.addEventListener('click', function () {
appState.onTokenReceived('token123');
function getToken(token) {
fetch('https://gymlink.freemyip.com/api/auth/authorize_client', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
GymKey: token, // Just testing token
id: '123',
}),
})
.then(res => res.json())
.catch(e => {
console.log(e);
setTimeout(getToken, 1000);
})
.then(data => {
if (data.payload) appState.checkToken(data.payload.token);
else {
console.log(data);
setTimeout(getToken, 1000);
}
});
}
const btn = document.getElementById('token');
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');
colorChangeBtnRed.addEventListener('click', function () {
var hexColor = '#FF0000'
.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 hexColor = '#FF0000'.substring(1);
var numColor = parseInt(hexColor, 16);
appState.changeColor(numColor);
appState.changeColor(numColor, true);
});
let colorChangeBtnBlue = document.getElementById('colorChangeBtnBlue');
colorChangeBtnBlue.addEventListener('click', function () {
var hexColor = '#0000FF'
.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 hexColor = '#0000FF'.substring(1);
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() {
console.error('aboba');
console.error('Error');
}
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>