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 { android {
namespace "com.example.flutter_application_1" namespace "com.example.gym_app"
compileSdk flutter.compileSdkVersion compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
@@ -42,11 +42,11 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // 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. // 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,6 +1,7 @@
<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="flutter_application_1" android:label="Example Gym App"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity
@@ -10,6 +11,7 @@
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:enableOnBackInvokedCallback="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user 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 import io.flutter.embedding.android.FlutterActivity

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

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; 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 { 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;
@@ -32,52 +34,58 @@ class BasketItemCard extends StatelessWidget {
minWidth: 400, minWidth: 400,
maxWidth: 600, maxWidth: 600,
), ),
child: Card( child: GestureDetector(
elevation: 4, onTap: () {
color: Theme.of(context).scaffoldBackgroundColor, Navigator.of(context).push(
shape: RoundedRectangleBorder( CustomPageRoute(builder: (context) => DetailPage(id: id)));
borderRadius: BorderRadius.circular(30), },
), child: Card(
child: Padding( elevation: 4,
padding: const EdgeInsetsDirectional.symmetric(horizontal: 20), color: Theme.of(context).scaffoldBackgroundColor,
child: Row( shape: RoundedRectangleBorder(
mainAxisAlignment: MainAxisAlignment.spaceBetween, borderRadius: BorderRadius.circular(30),
crossAxisAlignment: CrossAxisAlignment.center, ),
children: [ child: Padding(
Row( padding: const EdgeInsetsDirectional.symmetric(horizontal: 20),
crossAxisAlignment: CrossAxisAlignment.center, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
image, crossAxisAlignment: CrossAxisAlignment.center,
const SizedBox(width: 20), children: [
Column( Row(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( image,
name, const SizedBox(width: 20),
style: Theme.of(context).textTheme.bodyLarge, Column(
), mainAxisSize: MainAxisSize.min,
Text('\$$price'), children: [
], Text(
) name,
], style: Theme.of(context).textTheme.bodyLarge,
), ),
Row( Text('$price руб.'),
mainAxisSize: MainAxisSize.min, ],
children: [ )
IconButton( ],
icon: const Icon(Icons.remove), ),
onPressed: onTapMinus, Row(
), mainAxisSize: MainAxisSize.min,
const SizedBox(width: 10), children: [
Text(quantity), IconButton(
const SizedBox(width: 10), icon: const Icon(Icons.remove),
IconButton( onPressed: onTapMinus,
icon: const Icon(Icons.add), ),
onPressed: onTapPlus, 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: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,38 +38,42 @@ class HistoryItemCard extends StatelessWidget {
minWidth: 600, minWidth: 600,
maxWidth: 800, maxWidth: 800,
), ),
child: Card( child: GestureDetector(
elevation: 4, onTap: () {
color: Theme.of(context).scaffoldBackgroundColor, Navigator.of(context).push(
shape: RoundedRectangleBorder( CustomPageRoute(builder: (context) => OrderInfoPage(id: id)));
borderRadius: BorderRadius.circular(30), },
), child: Card(
child: Padding( elevation: 4,
padding: const EdgeInsetsDirectional.symmetric(horizontal: 20), color: Theme.of(context).scaffoldBackgroundColor,
child: Row( shape: RoundedRectangleBorder(
mainAxisAlignment: MainAxisAlignment.start, borderRadius: BorderRadius.circular(30),
crossAxisAlignment: CrossAxisAlignment.center, ),
children: [ child: Padding(
Row( padding: const EdgeInsetsDirectional.symmetric(horizontal: 20),
crossAxisAlignment: CrossAxisAlignment.center, child: Row(
children: [ mainAxisAlignment: MainAxisAlignment.start,
image, crossAxisAlignment: CrossAxisAlignment.center,
const SizedBox(width: 20), children: [
Column( Row(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ image,
MarkdownBody( const SizedBox(width: 20),
data: '### Заказ **№$id** от $date', Column(
), mainAxisSize: MainAxisSize.min,
MarkdownBody( crossAxisAlignment: CrossAxisAlignment.start,
data: 'Статус: **${orderStatusMap[status]}**'), children: [
MarkdownBody(data: 'Сумма: **\$$cost**'), MarkdownBody(
], data: '### Заказ **№$id** от $date',
) ),
], MarkdownBody(data: 'Сумма: **$cost руб.**'),
), ],
], )
],
),
],
),
), ),
), ),
), ),

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,11 +45,12 @@ 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(
'\$$price', '$price руб.',
style: Theme.of(context).textTheme.titleSmall, style: Theme.of(context).textTheme.titleSmall,
), ),
], ],

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')
],
),
),
),
),
);
}
}

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 '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: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 {
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 { class MyExampleApp extends StatelessWidget {
const MyExampleApp({super.key}); const MyExampleApp({super.key});
@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,
@@ -35,18 +56,49 @@ Widget getDrawer(BuildContext context) => Drawer(
children: [ children: [
const DrawerHeader(child: Text('Drawer Header')), const DrawerHeader(child: Text('Drawer Header')),
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: () {
MaterialPageRoute( Future.microtask(() async {
builder: (context) => const ExampleMainPage(), 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( 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(),
)), )),
), ),
@@ -71,28 +123,54 @@ class _ExamplePageState extends State<ExamplePage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
Future.microtask( // Future.microtask(
() => context.read<GymLinkProvider>().onTokenReceived('token123')); // () => context.read<GymLinkProvider>().onTokenReceived('token123'));
Future.microtask(() => context Future.microtask(() => context
.read<GymLinkProvider>() .read<GymLinkProvider>()
.setTheme(ThemeData.light(useMaterial3: true))); .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 @override
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(
@@ -104,13 +182,73 @@ class _ExamplePageState extends State<ExamplePage> {
const Text('Bottom text') const Text('Bottom text')
], ],
), ),
floatingActionButton: IconButton( );
icon: const Icon(Icons.search), }
onPressed: () { }
context
.read<GymLinkProvider>() class ExampleClub2Page extends StatefulWidget {
.changeTheme(Random().nextInt(0xffffff + 1)); 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'), 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

@@ -2,11 +2,14 @@ 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/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/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/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: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';
@@ -56,62 +59,71 @@ class BasketPage extends StatefulWidget {
} }
class _BasketPageState extends State<BasketPage> { class _BasketPageState extends State<BasketPage> {
List<Map<String, dynamic>> cartItems = []; List<GymItem> cartItems = [];
int totalPrice = 0; double totalPrice = 0;
List<GymItem> gymCart = [];
bool _isLoading = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
getCart().then((value) { Future.microtask(() => getCart().then((value) async {
setState(() { final itemIds =
cartItems = value.map((element) { value.map((element) => element['id'] as String).toList();
final item = cart.firstWhere((e) => e['id'] == element['id']); final items = await getItemsByIds(context, itemIds);
return {...item, 'count': element['count'] as int}; setState(() {
}).toList(); gymCart = items;
totalPrice = cartItems.fold( cartItems = value.map((element) {
0, final item = gymCart.firstWhere((e) => e.id == element['id']);
(sum, item) => item.localCount = element['count'] as int;
sum + int.parse(item['price']) * item['count'] as int); return item;
}); }).toList();
}); totalPrice = cartItems.fold(
0, (sum, item) => sum + item.price * item.localCount);
_isLoading = false;
});
}));
} }
void _updateCart() { void _updateCart() {
Provider.of<CartProvider>(context, listen: false).updateCartLength(); Provider.of<CartProvider>(context, listen: false).updateCartLength();
Provider.of<GymLinkProvider>(context, listen: false).onTokenReceived('123');
} }
void removeItem(String id) async { void removeItem(String id) async {
final item = cartItems.firstWhere((element) => element['id'] == id); final item = cartItems.firstWhere((element) => element.id == id);
bool toDelete = false; bool toDelete = false;
setState(() { setState(() {
if (item['count'] > 1) { if (item.localCount > 1) {
item['count']--; item.localCount--;
cartItems[cartItems.indexOf(item)]['count'] = item['count']; cartItems[cartItems.indexOf(item)].localCount = item.localCount;
} else { } else {
toDelete = true; toDelete = true;
} }
totalPrice = cartItems.fold(0, totalPrice =
(sum, item) => sum + int.parse(item['price']) * item['count'] as int); cartItems.fold(0, (sum, item) => sum + item.price * item.localCount);
}); });
if (toDelete) { if (toDelete) {
await _deleteItemAlert(id, item['name']); await _deleteItemAlert(id, item.title);
} else { } else {
await removeItemFromCart(id); await removeItemFromCart(id);
} }
} }
void addItem(String id) async { 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(() { setState(() {
final item = cartItems.firstWhere((element) => element['id'] == id, item.localCount++;
orElse: () => { cartItems[cartItems.indexOf(item)].localCount = item.localCount;
...cart.firstWhere((element) => element['id'] == id), totalPrice =
'count': 0 cartItems.fold(0, (sum, item) => sum + item.price * item.localCount);
});
item['count']++;
cartItems[cartItems.indexOf(item)]['count'] = item['count'];
totalPrice = cartItems.fold(0,
(sum, item) => sum + int.parse(item['price']) * item['count'] as int);
}); });
await addItemToCart(id); await addItemToCart(id);
} }
@@ -142,7 +154,9 @@ class _BasketPageState extends State<BasketPage> {
onPressed: () { onPressed: () {
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();
@@ -224,76 +238,24 @@ class _BasketPageState extends State<BasketPage> {
body: Column( body: Column(
children: [ children: [
const GymLinkHeader(title: "Корзина"), const GymLinkHeader(title: "Корзина"),
cartItems.isEmpty _isLoading
? Expanded( ? const Expanded(
child: Center( child: Center(child: CircularProgressIndicator()))
child: Column( : cartItems.isEmpty
mainAxisAlignment: MainAxisAlignment.center, ? Expanded(
children: [ child: Center(
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),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text('Корзина пуста',
'Итого: $totalPrice', style: Theme.of(context).textTheme.bodyLarge),
), const SizedBox(height: 10),
ElevatedButton( ElevatedButton(
onPressed: () => Navigator.of(context).push( onPressed: () => Navigator.pushAndRemoveUntil(
CustomPageRoute( context,
builder: (context) => CustomPageRoute(
const OrderConfirmationPage(), 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(
@@ -302,27 +264,102 @@ class _BasketPageState extends State<BasketPage> {
), ),
foregroundColor: Colors.white, foregroundColor: Colors.white,
), ),
child: const Text('Оформить заказ'), 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), )
], : 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/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/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/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/text.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
//TODO: Сделать получение инфы через объект
class DetailPage extends StatefulWidget { class DetailPage extends StatefulWidget {
final String name;
final String description;
final String price;
final String id; final String id;
final Image image;
const DetailPage({ const DetailPage({
super.key, super.key,
required this.name,
required this.description,
required this.price,
required this.id, required this.id,
required this.image,
}); });
@override @override
@@ -28,10 +31,13 @@ class DetailPage extends StatefulWidget {
class _DetailPageState extends State<DetailPage> { class _DetailPageState extends State<DetailPage> {
bool isInCart = false; bool isInCart = false;
int quantity = 0; int quantity = 0;
GymItem? item;
String? categoryName;
final CarouselController _carouselController = CarouselController();
int _currentImage = 0;
@override @override
void initState() { void initState() {
super.initState();
getCart().then((value) { getCart().then((value) {
setState(() { setState(() {
isInCart = value.any((element) => element['id'] == widget.id); 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( Widget _buildRowOrCol(
@@ -48,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,
@@ -83,117 +117,245 @@ class _DetailPageState extends State<DetailPage> {
child: const Text('Добавить в корзину'), child: const Text('Добавить в корзину'),
); );
} else { } else {
return Row( return Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( Row(
icon: const Icon(Icons.remove), mainAxisSize: MainAxisSize.min,
onPressed: () async { children: [
await removeItemFromCart(widget.id); IconButton(
setState(() { icon: const Icon(Icons.remove),
if (quantity > 1) { onPressed: () async {
quantity--; await removeItemFromCart(widget.id);
} else { setState(() {
isInCart = false; if (quantity > 1) {
quantity = 0; quantity--;
} } else {
}); isInCart = false;
if (mounted) { quantity = 0;
context.read<CartProvider>().updateCartLength(); }
} });
}, if (mounted) {
), context.read<CartProvider>().updateCartLength();
const SizedBox(width: 10), }
Text('$quantity'), },
const SizedBox(width: 10), ),
IconButton( const SizedBox(width: 10),
icon: const Icon(Icons.add), Text('$quantity'),
onPressed: () async { const SizedBox(width: 10),
await addItemToCart(widget.id); IconButton(
setState(() { icon: const Icon(Icons.add),
quantity++; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: const GymLinkAppBar(), appBar: const GymLinkAppBar(),
body: Column(mainAxisAlignment: MainAxisAlignment.start, children: [ body: item != null
GymLinkHeader(title: widget.name), ? Column(mainAxisAlignment: MainAxisAlignment.start, children: [
Expanded( GymLinkHeader(title: shortString(item!.title, length: 20)),
child: SingleChildScrollView( Expanded(
child: Padding( child: SingleChildScrollView(
padding: const EdgeInsets.all(20), child: Padding(
child: SizedBox( padding: const EdgeInsets.all(20),
width: MediaQuery.sizeOf(context).width, child: SizedBox(
// height: MediaQuery.sizeOf(context).height, width: MediaQuery.sizeOf(context).width,
child: _buildRowOrCol( child: _buildRowOrCol(
context: context, context: context,
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
widget.image, item!.images.length > 1
Padding( ? Column(children: [
padding: const EdgeInsetsDirectional.all(30), CarouselSlider.builder(
child: ConstrainedBox( itemCount: item!.images.length,
constraints: const BoxConstraints( itemBuilder: (context, index, realIdx) {
minWidth: 340, return Center(
maxWidth: 340, child: Image.network(
maxHeight: 600, item!.images[index].url,
), width: min(
child: Card( 550,
elevation: 4, MediaQuery.sizeOf(context)
color: Theme.of(context).scaffoldBackgroundColor, .width)),
shape: RoundedRectangleBorder( );
borderRadius: BorderRadius.circular(16), },
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( child: Padding(
padding: const EdgeInsetsDirectional.all(15), padding: const EdgeInsets.symmetric(vertical: 10),
child: ConstrainedBox( child: Chip(
constraints: const BoxConstraints( label: Text(categoryName != null
minHeight: 100, ? (categoryName == ""
), ? "Без категории"
child: Text( : categoryName!)
widget.description, : ''),
style: Theme.of(context).textTheme.bodyMedium, backgroundColor: Colors.white,
), labelStyle:
const TextStyle(color: Colors.black),
), ),
), ),
), ),
), Center(
), child: MarkdownBody(
), data: '### Остаток: _${item!.count}_',
Align( )),
alignment: const AlignmentDirectional(0, -1), item!.description != ''
child: Padding( ? Padding(
padding: padding: const EdgeInsetsDirectional.all(30),
const EdgeInsetsDirectional.fromSTEB(0, 30, 0, 0), child: ConstrainedBox(
child: Column( constraints: const BoxConstraints(
mainAxisSize: MainAxisSize.min, minWidth: 340,
children: [ maxWidth: 340,
Text( maxHeight: 600,
'Стоимость ${widget.price}', ),
style: Theme.of(context).textTheme.bodyLarge, 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: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/item_card.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/basket.dart';
import 'package:gymlink_module_web/pages/detail.dart'; import 'package:gymlink_module_web/pages/detail.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/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/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 = [
{ {
@@ -65,48 +65,74 @@ class MainPage extends StatefulWidget {
class _MainPageState extends State<MainPage> { class _MainPageState extends State<MainPage> {
String searchText = ''; String searchText = '';
List<Map<String, String>> filteredData = []; List<GymItem> filteredData = [];
int cartLength = 0; 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 @override
void initState() { void initState() {
super.initState(); super.initState();
filteredData = testData;
getCart().then((value) { getCart().then((value) {
setState(() { setState(() {
cartLength = value.length; cartLength = value.length;
}); });
}).whenComplete(() {
if (mounted) {
setState(() {});
}
}); });
} getCategories(context).then((value) => setState(() {
categories = value;
Future<void> _goToPage() async { _onSearch();
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 {
await Future.delayed(const Duration(milliseconds: 1000)); if (itemViewCount < filteredData.length) {
debugPrint('aye'); 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() { void _onSearch() {
final categoryId = selectedCategory == null ? '' : selectedCategory!.id;
setState(() { setState(() {
filteredData = testData searchText = _searchField.text.trim().toLowerCase();
.where((element) => (element['name']!).contains(searchText))
.toList();
}); });
_searchItems(searchText: searchText, categoryId: categoryId);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cartL = context.watch<CartProvider>().cartLength; final cartL = context.watch<CartProvider>().cartLength;
final onError = context.read<GymLinkProvider>().onError;
return Scaffold( return Scaffold(
appBar: const GymLinkAppBar(), appBar: const GymLinkAppBar(),
body: Column( body: Column(
@@ -119,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(
@@ -154,13 +183,12 @@ class _MainPageState extends State<MainPage> {
), ),
), ),
), ),
getSpacer(context: context, flex: 2), // getSpacer(context: context, flex: 2),
const SizedBox( const SizedBox(
width: 8, width: 8,
), ),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
onError();
Navigator.of(context).push(CustomPageRoute( Navigator.of(context).push(CustomPageRoute(
builder: (context) => const HistoryPage(), 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( Expanded(
child: LazyLoadScrollView( child: LazyLoadScrollView(
onEndOfPage: _onLoad, onEndOfPage: _onLoad,
child: Stack( isLoading: isLoading,
children: [ child: Scrollbar(
GridView.builder( controller: _scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( child: ListView(
crossAxisCount: min( controller: _scrollController,
(MediaQuery.sizeOf(context).width ~/ 200).toInt(), 8), children: [
), filteredData.isEmpty &&
itemCount: filteredData.length, (searchText != '' || selectedCategory != null) &&
itemBuilder: (context, index) { !isSearching
final product = filteredData[index]; ? const Center(child: Text('Ничего не найдено'))
return ProductCard( : isSearching
imagePath: Image( ? const Center(child: CircularProgressIndicator())
image: Image.network( : GridView.builder(
'https://rus-sport.net/upload/iblock/311/topb85ez18pq0aavohpa5zipk2sbfxll.jpg') physics: const NeverScrollableScrollPhysics(),
.image, shrinkWrap: true,
width: 50, gridDelegate:
), SliverGridDelegateWithFixedCrossAxisCount(
name: product['name']!, crossAxisCount: min(
price: product['price']!, (MediaQuery.sizeOf(context).width ~/
onTap: () => Navigator.of(context).push( 220)
CustomPageRoute( .toInt(),
builder: (context) => DetailPage( 8),
name: product['name']!, childAspectRatio: 0.8,
description: product['details']!, mainAxisSpacing: 10.0,
price: product['price']!, crossAxisSpacing: 20.0),
id: product['id']!, itemCount: itemViewCount,
image: Image( itemBuilder: (context, index) {
image: final product = filteredData[index];
AssetImage('assets/${product['image']!}'), return ProductCard(
width: 300), 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/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';
import 'package:gymlink_module_web/components/heading.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/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/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/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';
@@ -56,33 +62,154 @@ class OrderConfirmationPage extends StatefulWidget {
} }
class _OrderConfirmationPageState extends State<OrderConfirmationPage> { class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
List<Map<String, dynamic>> cartItems = []; List<GymItem> cartItems = [];
int totalPrice = 0; 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 @override
void initState() { void initState() {
super.initState(); super.initState();
getCart().then((value) { Future.microtask(() => getCart().then((value) async {
setState(() { final itemIds =
cartItems = value.map((element) { value.map((element) => element['id'] as String).toList();
final item = cart.firstWhere((e) => e['id'] == element['id']); final items = await getItemsByIds(context, itemIds);
return {...item, 'count': element['count'] as int}; setState(() {
}).toList(); gymCart = items;
totalPrice = cartItems.fold( cartItems = value.map((element) {
0, final item = gymCart.firstWhere((e) => e.id == element['id']);
(sum, item) => item.localCount = element['count'] as int;
sum + int.parse(item['price']) * item['count'] as int); return item;
}); }).toList();
}); totalPrice = cartItems.fold(
0, (sum, item) => sum + item.price * item.localCount);
_isLoading = false;
});
}));
} }
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(
@@ -94,24 +221,39 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
const GymLinkHeader(title: 'Оформление заказа'), const GymLinkHeader(title: 'Оформление заказа'),
const MarkdownBody(data: '## Состав заказа:'), const MarkdownBody(data: '## Состав заказа:'),
Expanded( Expanded(
child: ConstrainedBox( child: _isLoading
constraints: const BoxConstraints(maxHeight: 350), ? const Center(child: CircularProgressIndicator())
child: ListView.builder( : ConstrainedBox(
itemCount: cartItems.length, constraints: const BoxConstraints(maxHeight: 350),
itemBuilder: (context, index) { child: ListView.builder(
final item = cartItems[index]; shrinkWrap: true,
return OrderConfirmItemCard( itemCount: cartItems.length,
name: item['name'], itemBuilder: (context, index) {
image: Image( final item = cartItems[index];
image: AssetImage('assets/${item['image']}'), return OrderConfirmItemCard(
width: 50, name: shortString(item.title),
height: 50), image: FutureBuilder(
price: double.parse(item['price']), future: precacheImage(
count: item['count'], 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( const SizedBox(
height: 10, height: 10,
@@ -119,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(
@@ -132,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(
@@ -143,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(
@@ -153,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,44 +90,100 @@ class _HistoryPageState extends State<HistoryPage> {
body: Column( body: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
const GymLinkHeader(title: 'История заказов'), const GymLinkHeader(
Expanded( title: 'История заказов',
child: Row( toMain: true,
children: [ ),
Expanded( const SizedBox(height: 5),
child: LazyLoadScrollView( kIsWeb && !_isLoading
onEndOfPage: _onLoad, ? Center(
scrollOffset: 200, child: ElevatedButton(
child: RefreshIndicator( onPressed: () {
edgeOffset: 55, setState(() => _isRefreshing = true);
onRefresh: () => _onRefresh(_updateTimer), _onRefresh(_updateTimer);
child: Stack( },
children: [ style: ElevatedButton.styleFrom(
ListView.builder( backgroundColor: Theme.of(context).primaryColor,
itemCount: my_orders.length, shape: const RoundedRectangleBorder(
itemBuilder: (context, index) { borderRadius: BorderRadius.all(Radius.circular(50)),
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,
);
},
),
],
), ),
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 get isLoading => _isLoading;
bool _blackTheme = false; bool _blackTheme = false;
bool get blackTheme => _blackTheme; bool get blackTheme => _blackTheme;
String _token = '';
String get token => _token;
ThemeData _theme = myTheme; ThemeData _theme = myTheme;
ThemeData get theme => _theme; ThemeData get theme => _theme;
void Function() _onError = () => {}; void Function() _onError = () => {};
void Function() get onError => _onError; void Function() get onError => _onError;
void onTokenReceived(String token) { void checkToken(String token) {
if (token == 'token123') { _token = token;
_isLoading = false; _isLoading = false;
notifyListeners(); 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();
} }
@@ -34,6 +32,11 @@ class GymLinkProvider with ChangeNotifier {
} }
void setOnError(void Function() onError) { 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( : 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));
}

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" 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

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

View File

@@ -1,7 +1,7 @@
<!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,54 +14,56 @@
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.
const serviceWorkerVersion = null; const serviceWorkerVersion = null;
</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>
<article> <button id="clearBtn">Clear</button>
<div id="flutter_target"></div> <section class="contents">
</article> <article>
</section> <div id="flutter_target"></div>
<script> </article>
window.addEventListener('load', function(ev) { </section>
let target = document.querySelector('#flutter_target'); <script>
_flutter.loader.loadEntrypoint({ window.addEventListener('load', function (ev) {
onEntrypointLoaded: async function (engineInitializer) { let target = document.querySelector('#flutter_target');
let appRunner = await engineInitializer.initializeEngine({ _flutter.loader.loadEntrypoint({
hostElement: target, onEntrypointLoaded: async function (engineInitializer) {
}); let appRunner = await engineInitializer.initializeEngine({
await appRunner.runApp(); hostElement: target,
} });
}) await appRunner.runApp();
}); },
</script> });
<script src='js/demo-js-interop.js' defer></script> });
</body> </script>
<script src="js/demo-js-interop.js" defer></script>
</body>
</html> </html>

View File

@@ -7,37 +7,62 @@
}; };
let appState = window._appState; let appState = window._appState;
let btn = document.getElementById('token'); function getToken(token) {
btn.addEventListener('click', function () { fetch('https://gymlink.freemyip.com/api/auth/authorize_client', {
appState.onTokenReceived('token123'); 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'); 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>