Compare commits

...

48 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
28db4ce298 Add: Clear search 2024-05-21 23:50:30 +03:00
30fdc0a144 Fix: Floating btn hover elevation 2024-05-21 23:50:18 +03:00
9ac9813244 OnPay operations 2024-05-21 22:36:42 +03:00
986a9d9bd5 Error provider 2024-05-17 16:11:05 +03:00
c54176212a Add: CartProvider 2024-05-15 13:49:18 +03:00
e52357edf5 Added: Some TODOs 2024-05-15 02:28:02 +03:00
464f51238f Add: Lazy loading and refresh on pull down 2024-05-15 00:34:05 +03:00
baf85776e9 Add: logo 2024-05-15 00:33:14 +03:00
34 changed files with 2608 additions and 702 deletions

View File

@@ -23,7 +23,7 @@ if (flutterVersionName == null) {
}
android {
namespace "com.example.flutter_application_1"
namespace "com.example.gym_app"
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
@@ -42,11 +42,11 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.flutter_application_1"
applicationId "com.example.gym_app"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}

View File

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

View File

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

14
assets/icon.svg Normal file
View File

@@ -0,0 +1,14 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1930_2195)">
<path d="M13.2378 0.536444C13.2378 0.832951 13.5486 1.07336 13.9311 1.07336C14.9923 1.07336 17.0092 1.07336 18.0698 1.07336C18.4523 1.07336 18.7624 0.83289 18.7624 0.535822C18.7624 0.239315 18.4522 -3.74317e-05 18.0698 -3.74317e-05C17.0099 -3.74317e-05 14.9928 -3.74317e-05 13.9305 -3.74317e-05C13.5486 -3.74317e-05 13.2378 0.239938 13.2378 0.536444Z" fill="black"/>
<path d="M13.2378 31.4624C13.2378 31.7589 13.5486 32 13.9311 32C14.9935 32 17.0074 32 18.0685 31.9994C18.451 31.9994 18.7612 31.7589 18.7625 31.4624C18.7625 31.1658 18.4523 30.926 18.0698 30.926C17.0093 30.926 14.9929 30.926 13.9305 30.9254C13.5486 30.9254 13.2378 31.1658 13.2378 31.4624Z" fill="black"/>
<path d="M6.96338 24.3076C6.96338 25.2968 7.89411 26.0993 9.04154 26.0993C12.5199 26.0993 19.4814 26.0987 22.9585 26.0981C24.1059 26.0981 25.0366 25.2968 25.0366 24.3075C25.0366 23.3182 24.1053 22.5157 22.9585 22.5157C21.5944 22.5157 19.6919 22.5157 17.6761 22.5157C17.6761 21.4829 17.6761 12.2383 17.6761 9.4828C19.6925 9.4828 21.5944 9.4828 22.9591 9.4828C24.1059 9.4828 25.0366 8.68032 25.0366 7.69097C25.0366 6.70174 24.1053 5.90038 22.9585 5.90038C19.4801 5.90038 12.5186 5.90038 9.04154 5.90038C7.89354 5.90038 6.96338 6.7018 6.96338 7.69097C6.96338 8.68026 7.8929 9.4828 9.04154 9.4828C10.4056 9.4828 12.3075 9.4828 14.3238 9.4828C14.3238 12.2393 14.3238 21.4829 14.3238 22.5157C12.3075 22.5157 10.4055 22.5157 9.0409 22.5157C7.89296 22.5157 6.96338 23.3177 6.96338 24.3076Z" fill="black"/>
<path d="M9.73926 3.39964C9.73926 3.45817 9.73926 3.51601 9.73926 3.57504C9.73926 4.32036 10.358 4.92535 11.1235 4.92535C13.5327 4.92535 18.4675 4.92535 20.8766 4.92535C21.6415 4.92535 22.2621 4.32042 22.2621 3.5746C22.2621 3.51607 22.2621 3.45699 22.2621 3.39914C22.2621 2.65332 21.6434 2.04839 20.8778 2.04839C18.4693 2.04839 13.5339 2.04839 11.1246 2.04889C10.3592 2.04827 9.73926 2.65382 9.73926 3.39964Z" fill="black"/>
<path d="M9.73926 28.424C9.73926 28.4824 9.73926 28.5415 9.73926 28.6C9.73926 29.3458 10.358 29.9503 11.1235 29.9503C13.5327 29.9503 18.4675 29.9503 20.8766 29.9503C21.6422 29.9503 22.2621 29.3458 22.2621 28.6C22.2621 28.5415 22.2621 28.4824 22.2621 28.4246C22.2621 27.6788 21.6434 27.0744 20.8778 27.0744C18.4693 27.0744 13.5339 27.0744 11.1246 27.0744C10.3592 27.0737 9.73926 27.6782 9.73926 28.424Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_1930_2195">
<rect width="32" height="32" fill="white" transform="matrix(0 -1 1 0 0 32)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class GymLinkAppBar extends StatelessWidget implements PreferredSizeWidget {
const GymLinkAppBar({super.key});
@@ -10,14 +11,23 @@ class GymLinkAppBar extends StatelessWidget implements PreferredSizeWidget {
shadowColor: null,
automaticallyImplyLeading: false,
elevation: 0,
scrolledUnderElevation: 4,
scrolledUnderElevation: 0,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Padding(
padding: EdgeInsets.only(right: 8),
child: Image(
image: AssetImage('assets/logo.png'), width: 24, height: 24),
Padding(
padding: const EdgeInsets.only(right: 8),
child: SvgPicture.asset(
'assets/icon.svg',
width: 24,
height: 24,
semanticsLabel: 'GymLink Logo',
colorFilter: ColorFilter.mode(
Theme.of(context).brightness == Brightness.dark
? Colors.white
: Colors.black,
BlendMode.srcIn),
),
),
Align(
alignment: Alignment.centerRight,

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/pages/detail.dart';
import 'package:gymlink_module_web/tools/routes.dart';
class BasketItemCard extends StatelessWidget {
final String name;
final String price;
final String id;
final Image image;
final Widget image;
final String quantity;
final VoidCallback onTapPlus;
final VoidCallback onTapMinus;
@@ -32,52 +34,58 @@ class BasketItemCard extends StatelessWidget {
minWidth: 400,
maxWidth: 600,
),
child: Card(
elevation: 4,
color: Theme.of(context).scaffoldBackgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
child: Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
image,
const SizedBox(width: 20),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
name,
style: Theme.of(context).textTheme.bodyLarge,
),
Text('\$$price'),
],
)
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: onTapMinus,
),
const SizedBox(width: 10),
Text(quantity),
const SizedBox(width: 10),
IconButton(
icon: const Icon(Icons.add),
onPressed: onTapPlus,
),
],
)
],
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
CustomPageRoute(builder: (context) => DetailPage(id: id)));
},
child: Card(
elevation: 4,
color: Theme.of(context).scaffoldBackgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
child: Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
image,
const SizedBox(width: 20),
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
name,
style: Theme.of(context).textTheme.bodyLarge,
),
Text('$price руб.'),
],
)
],
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: onTapMinus,
),
const SizedBox(width: 10),
Text(quantity),
const SizedBox(width: 10),
IconButton(
icon: const Icon(Icons.add),
onPressed: onTapPlus,
),
],
)
],
),
),
),
),

View File

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

View File

@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:gymlink_module_web/pages/order_info.dart';
import 'package:gymlink_module_web/tools/routes.dart';
enum OrderStatus { created, inProgress, completed, canceled }
@@ -14,15 +16,13 @@ class HistoryItemCard extends StatelessWidget {
final String id;
final String cost;
final String date;
final Image image;
final OrderStatus status;
final Widget image;
const HistoryItemCard({
super.key,
required this.id,
required this.cost,
required this.date,
required this.status,
required this.image,
});
@@ -38,38 +38,42 @@ class HistoryItemCard extends StatelessWidget {
minWidth: 600,
maxWidth: 800,
),
child: Card(
elevation: 4,
color: Theme.of(context).scaffoldBackgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
child: Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
image,
const SizedBox(width: 20),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownBody(
data: '### Заказ **№$id** от $date',
),
MarkdownBody(
data: 'Статус: **${orderStatusMap[status]}**'),
MarkdownBody(data: 'Сумма: **\$$cost**'),
],
)
],
),
],
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
CustomPageRoute(builder: (context) => OrderInfoPage(id: id)));
},
child: Card(
elevation: 4,
color: Theme.of(context).scaffoldBackgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
child: Padding(
padding: const EdgeInsetsDirectional.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
image,
const SizedBox(width: 20),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownBody(
data: '### Заказ **№$id** от $date',
),
MarkdownBody(data: 'Сумма: **$cost руб.**'),
],
)
],
),
],
),
),
),
),

View File

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

View File

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

View File

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

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

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

View File

@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/providers/main.dart';
import 'package:gymlink_module_web/states/web.dart';
import 'package:provider/provider.dart';
void main() {
runApp(const MyApp());
runApp(const MyAppWithProvider());
}
class MyApp extends StatefulWidget {
@@ -11,3 +13,13 @@ class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => MyAppStateWeb();
}
class MyAppWithProvider extends StatelessWidget {
const MyAppWithProvider({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => GymLinkProvider(), child: const MyApp());
}
}

View File

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

View File

@@ -2,8 +2,16 @@ import 'package:flutter/material.dart';
import 'package:gymlink_module_web/components/app_bar.dart';
import 'package:gymlink_module_web/components/basket_item_card.dart';
import 'package:gymlink_module_web/components/heading.dart';
import 'package:gymlink_module_web/interfaces/items.dart';
import 'package:gymlink_module_web/pages/main.dart';
import 'package:gymlink_module_web/pages/order_confirmation.dart';
import 'package:gymlink_module_web/providers/cart.dart';
import 'package:gymlink_module_web/tools/items.dart';
import 'package:gymlink_module_web/tools/prefs.dart';
import 'package:gymlink_module_web/tools/routes.dart';
import 'package:gymlink_module_web/tools/text.dart';
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
import 'package:provider/provider.dart';
List<Map<String, dynamic>> cart = [
{
@@ -51,57 +59,71 @@ class BasketPage extends StatefulWidget {
}
class _BasketPageState extends State<BasketPage> {
List<Map<String, dynamic>> cartItems = [];
int totalPrice = 0;
List<GymItem> cartItems = [];
double totalPrice = 0;
List<GymItem> gymCart = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
getCart().then((value) {
setState(() {
cartItems = value.map((element) {
final item = cart.firstWhere((e) => e['id'] == element['id']);
return {...item, 'count': element['count'] as int};
}).toList();
totalPrice = cartItems.fold(
0,
(sum, item) =>
sum + int.parse(item['price']) * item['count'] as int);
});
});
Future.microtask(() => getCart().then((value) async {
final itemIds =
value.map((element) => element['id'] as String).toList();
final items = await getItemsByIds(context, itemIds);
setState(() {
gymCart = items;
cartItems = value.map((element) {
final item = gymCart.firstWhere((e) => e.id == element['id']);
item.localCount = element['count'] as int;
return item;
}).toList();
totalPrice = cartItems.fold(
0, (sum, item) => sum + item.price * item.localCount);
_isLoading = false;
});
}));
}
void _updateCart() {
Provider.of<CartProvider>(context, listen: false).updateCartLength();
}
void removeItem(String id) async {
final item = cartItems.firstWhere((element) => element['id'] == id);
final item = cartItems.firstWhere((element) => element.id == id);
bool toDelete = false;
setState(() {
if (item['count'] > 1) {
item['count']--;
cartItems[cartItems.indexOf(item)]['count'] = item['count'];
if (item.localCount > 1) {
item.localCount--;
cartItems[cartItems.indexOf(item)].localCount = item.localCount;
} else {
toDelete = true;
}
totalPrice = cartItems.fold(0,
(sum, item) => sum + int.parse(item['price']) * item['count'] as int);
totalPrice =
cartItems.fold(0, (sum, item) => sum + item.price * item.localCount);
});
if (toDelete) {
await _deleteItemAlert(id, item['name']);
await _deleteItemAlert(id, item.title);
} else {
await removeItemFromCart(id);
}
}
void addItem(String id) async {
final item =
cartItems.firstWhere((element) => element.id == id, orElse: () {
final cartItem = gymCart.firstWhere((element) => element.id == id);
cartItem.localCount = 0;
return cartItem;
});
if (item.localCount + 1 > item.count) {
return;
}
setState(() {
final item = cartItems.firstWhere((element) => element['id'] == id,
orElse: () => {
...cart.firstWhere((element) => element['id'] == id),
'count': 0
});
item['count']++;
cartItems[cartItems.indexOf(item)]['count'] = item['count'];
totalPrice = cartItems.fold(0,
(sum, item) => sum + int.parse(item['price']) * item['count'] as int);
item.localCount++;
cartItems[cartItems.indexOf(item)].localCount = item.localCount;
totalPrice =
cartItems.fold(0, (sum, item) => sum + item.price * item.localCount);
});
await addItemToCart(id);
}
@@ -132,8 +154,13 @@ class _BasketPageState extends State<BasketPage> {
onPressed: () {
removeItemFromCart(id);
setState(() {
cartItems.removeWhere((element) => element['id'] == id);
cartItems.removeWhere((element) => element.id == id);
totalPrice = cartItems.fold(
0, (sum, item) => sum + item.price * item.localCount);
});
if (mounted) {
_updateCart();
}
Navigator.of(context).pop();
},
),
@@ -171,6 +198,9 @@ class _BasketPageState extends State<BasketPage> {
setState(() {
cartItems = [];
});
if (mounted) {
_updateCart();
}
Navigator.of(context).pop();
},
),
@@ -195,6 +225,12 @@ class _BasketPageState extends State<BasketPage> {
return const SizedBox(height: 10);
}
void _onLoad() async {
await Future.delayed(const Duration(microseconds: 1000));
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -202,72 +238,24 @@ class _BasketPageState extends State<BasketPage> {
body: Column(
children: [
const GymLinkHeader(title: "Корзина"),
cartItems.isEmpty
? Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Корзина пуста',
style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () => Navigator.pop(context),
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: ListView.builder(
itemCount: cartItems.length,
itemBuilder: (context, index) {
final item = cartItems[index];
return BasketItemCard(
name: item['name'],
price: item['price'],
id: item['id'],
image: Image(
image: AssetImage('assets/${item['image']}'),
width: 50,
),
onTapPlus: () => addItem(item['id'].toString()),
onTapMinus: () =>
removeItem(item['id'].toString()),
quantity: item['count'].toString(),
);
},
),
),
_buildSpacer(),
Padding(
padding: const EdgeInsetsDirectional.symmetric(
horizontal: 10, vertical: 10),
_isLoading
? const Expanded(
child: Center(child: CircularProgressIndicator()))
: cartItems.isEmpty
? Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Итого: $totalPrice',
),
Text('Корзина пуста',
style: Theme.of(context).textTheme.bodyLarge),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
const OrderConfirmationPage(),
),
),
onPressed: () => Navigator.pushAndRemoveUntil(
context,
CustomPageRoute(
builder: (_) => const MainPage()),
(route) => route.isFirst),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
shape: const RoundedRectangleBorder(
@@ -276,27 +264,102 @@ class _BasketPageState extends State<BasketPage> {
),
foregroundColor: Colors.white,
),
child: const Text('Оформить заказ'),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: _clearCartAlert,
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
shape: const RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(50))),
foregroundColor: Colors.white,
),
child: const Text('Очистить корзину'),
child: const Text('Вернуться назад'),
),
],
),
),
const SizedBox(width: 50),
],
),
),
)
: Expanded(
child: _buildRowOrCol(
context: context,
children: [
Expanded(
child: LazyLoadScrollView(
onEndOfPage: _onLoad,
child: ListView.builder(
itemCount: cartItems.length,
itemBuilder: (context, index) {
final item = cartItems[index];
return BasketItemCard(
name: shortString(item.title),
price: item.price.toStringAsFixed(2),
id: item.id,
image: FutureBuilder(
future: precacheImage(
NetworkImage(item.images[0].url),
context),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
return Image(
image: NetworkImage(
item.images[0].url),
width: 50,
);
} else {
return const CircularProgressIndicator();
}
},
),
onTapPlus: () =>
addItem(item.id.toString()),
onTapMinus: () {
removeItem(item.id.toString());
},
quantity: item.localCount.toString(),
);
},
),
),
),
// _buildSpacer(),
Padding(
padding: const EdgeInsetsDirectional.symmetric(
horizontal: 10, vertical: 10),
child: Column(
children: [
Text(
'Итого: ${totalPrice.toStringAsFixed(2)} руб.',
),
ElevatedButton(
onPressed: () => Navigator.of(context).push(
CustomPageRoute(
builder: (context) =>
const OrderConfirmationPage(),
),
),
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).primaryColor,
shape: const RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(50)),
),
foregroundColor: Colors.white,
),
child: const Text('Оформить заказ'),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: _clearCartAlert,
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).primaryColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(50))),
foregroundColor: Colors.white,
),
child: const Text('Очистить корзину'),
),
],
),
),
// const SizedBox(width: 50),
],
),
),
],
),
);

View File

@@ -1,22 +1,27 @@
import 'dart:convert';
import 'dart:math';
import 'package:carousel_slider/carousel_slider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:gymlink_module_web/components/app_bar.dart';
import 'package:gymlink_module_web/components/heading.dart';
import 'package:gymlink_module_web/interfaces/items.dart';
import 'package:gymlink_module_web/pages/basket.dart';
import 'package:gymlink_module_web/providers/cart.dart';
import 'package:gymlink_module_web/providers/main.dart';
import 'package:gymlink_module_web/tools/items.dart';
import 'package:gymlink_module_web/tools/prefs.dart';
import 'package:gymlink_module_web/tools/routes.dart';
import 'package:gymlink_module_web/tools/text.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
//TODO: Сделать получение инфы через объект
class DetailPage extends StatefulWidget {
final String name;
final String description;
final String price;
final String id;
final Image image;
const DetailPage({
super.key,
required this.name,
required this.description,
required this.price,
required this.id,
required this.image,
});
@override
@@ -26,10 +31,13 @@ class DetailPage extends StatefulWidget {
class _DetailPageState extends State<DetailPage> {
bool isInCart = false;
int quantity = 0;
GymItem? item;
String? categoryName;
final CarouselController _carouselController = CarouselController();
int _currentImage = 0;
@override
void initState() {
super.initState();
getCart().then((value) {
setState(() {
isInCart = value.any((element) => element['id'] == widget.id);
@@ -39,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(
@@ -46,12 +88,6 @@ class _DetailPageState extends State<DetailPage> {
required BuildContext context,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.spaceAround,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center}) {
// if (false && MediaQuery.of(context).size.width > 600) {
// return Row(
// mainAxisAlignment: mainAxisAlignment,
// crossAxisAlignment: crossAxisAlignment,
// children: children);
// }
return Column(
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
@@ -67,6 +103,9 @@ class _DetailPageState extends State<DetailPage> {
isInCart = true;
quantity = 1;
});
if (mounted) {
context.read<CartProvider>().updateCartLength();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
@@ -78,114 +117,245 @@ class _DetailPageState extends State<DetailPage> {
child: const Text('Добавить в корзину'),
);
} else {
return Row(
mainAxisSize: MainAxisSize.min,
return Column(
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () async {
await removeItemFromCart(widget.id);
setState(() {
if (quantity > 1) {
quantity--;
} else {
isInCart = false;
quantity = 0;
}
});
},
),
const SizedBox(width: 10),
Text('$quantity'),
const SizedBox(width: 10),
IconButton(
icon: const Icon(Icons.add),
onPressed: () async {
await addItemToCart(widget.id);
setState(() {
quantity++;
});
},
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove),
onPressed: () async {
await removeItemFromCart(widget.id);
setState(() {
if (quantity > 1) {
quantity--;
} else {
isInCart = false;
quantity = 0;
}
});
if (mounted) {
context.read<CartProvider>().updateCartLength();
}
},
),
const SizedBox(width: 10),
Text('$quantity'),
const SizedBox(width: 10),
IconButton(
icon: const Icon(Icons.add),
onPressed: () async {
if (item!.count > quantity) {
await addItemToCart(widget.id);
setState(() {
quantity++;
});
}
},
),
],
),
Padding(
padding: const EdgeInsets.only(top: 10),
child: ElevatedButton(
onPressed: () {
Navigator.pushReplacement(context,
CustomPageRoute(builder: (context) => const BasketPage()));
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(50)),
),
foregroundColor: Colors.white,
),
child: const Text('Открыть корзину'),
),
)
],
);
}
}
double _getAspectRatio() {
double width = MediaQuery.sizeOf(context).width;
double height = MediaQuery.sizeOf(context).height;
return max(width, height) / min(width, height);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const GymLinkAppBar(),
body: Column(mainAxisAlignment: MainAxisAlignment.start, children: [
GymLinkHeader(title: widget.name),
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20),
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
// height: MediaQuery.sizeOf(context).height,
child: _buildRowOrCol(
context: context,
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
widget.image,
Padding(
padding: const EdgeInsetsDirectional.all(30),
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 340,
maxWidth: 340,
maxHeight: 600,
),
child: Card(
elevation: 4,
color: Theme.of(context).scaffoldBackgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
body: item != null
? Column(mainAxisAlignment: MainAxisAlignment.start, children: [
GymLinkHeader(title: shortString(item!.title, length: 20)),
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20),
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
child: _buildRowOrCol(
context: context,
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
item!.images.length > 1
? Column(children: [
CarouselSlider.builder(
itemCount: item!.images.length,
itemBuilder: (context, index, realIdx) {
return Center(
child: Image.network(
item!.images[index].url,
width: min(
550,
MediaQuery.sizeOf(context)
.width)),
);
},
carouselController: _carouselController,
options: CarouselOptions(
enlargeCenterPage: true,
height: min(
MediaQuery.sizeOf(context).height -
100,
400),
enableInfiniteScroll: false,
onPageChanged: (index, reason) {
setState(() {
_currentImage = index;
});
}),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: item!.images
.asMap()
.entries
.map((entry) {
return GestureDetector(
onTap: () => _carouselController
.animateToPage(entry.key),
child: Container(
width: 12.0,
height: 12.0,
margin: const EdgeInsets.symmetric(
vertical: 8.0, horizontal: 4.0),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: (Theme.of(context)
.brightness ==
Brightness.dark
? Colors.white
: Colors.black)
.withOpacity(
_currentImage == entry.key
? 0.9
: 0.4)),
),
);
}).toList(),
),
])
: Image.network(
item!.images[0].url,
height: 400,
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Text(
item!.title,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
),
child: SingleChildScrollView(
Center(
child: Padding(
padding: const EdgeInsetsDirectional.all(15),
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 100,
),
child: Text(
widget.description,
style: Theme.of(context).textTheme.bodyMedium,
),
padding: const EdgeInsets.symmetric(vertical: 10),
child: Chip(
label: Text(categoryName != null
? (categoryName == ""
? "Без категории"
: categoryName!)
: ''),
backgroundColor: Colors.white,
labelStyle:
const TextStyle(color: Colors.black),
),
),
),
),
),
),
Align(
alignment: const AlignmentDirectional(0, -1),
child: Padding(
padding:
const EdgeInsetsDirectional.fromSTEB(0, 30, 0, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Стоимость ${widget.price}',
style: Theme.of(context).textTheme.bodyLarge,
Center(
child: MarkdownBody(
data: '### Остаток: _${item!.count}_',
)),
item!.description != ''
? Padding(
padding: const EdgeInsetsDirectional.all(30),
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 340,
maxWidth: 340,
maxHeight: 600,
),
child: Card(
elevation: 4,
color: Theme.of(context)
.scaffoldBackgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: SingleChildScrollView(
child: Padding(
padding:
const EdgeInsetsDirectional.all(
15),
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 100,
),
child: Text(
item!.description,
style: Theme.of(context)
.textTheme
.bodyMedium,
),
),
),
),
),
),
)
: const SizedBox.shrink(),
Align(
alignment: const AlignmentDirectional(0, -1),
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(
0, 30, 0, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Стоимость ${item!.price.toStringAsFixed(2)}руб.',
style:
Theme.of(context).textTheme.bodyLarge,
),
_buildButton()
],
),
),
_buildButton()
],
),
),
],
),
),
],
),
),
),
])
: const Center(
child: CircularProgressIndicator(),
),
),
),
]),
);
}
}

View File

@@ -1,15 +1,23 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/components/app_bar.dart';
import 'package:gymlink_module_web/components/item_card.dart';
import 'package:gymlink_module_web/interfaces/items.dart';
import 'package:gymlink_module_web/pages/basket.dart';
import 'package:gymlink_module_web/pages/detail.dart';
import 'package:gymlink_module_web/pages/order_history.dart';
import 'package:gymlink_module_web/tools/relative.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:gymlink_module_web/providers/cart.dart';
import 'package:gymlink_module_web/tools/items.dart';
import 'package:gymlink_module_web/tools/prefs.dart';
import 'package:gymlink_module_web/tools/routes.dart';
import 'package:gymlink_module_web/tools/text.dart';
import 'package:lazy_load_scrollview/lazy_load_scrollview.dart';
import 'package:provider/provider.dart';
const List<Map<String, String>> testData = [
{
"name": "Протеин",
"name": "Протеин 2",
"image": "product.png",
"price": "120",
"details": "Test details",
@@ -47,11 +55,8 @@ const List<Map<String, String>> testData = [
];
class MainPage extends StatefulWidget {
final bool isLoading;
const MainPage({
super.key,
required this.isLoading,
});
@override
@@ -59,151 +64,346 @@ class MainPage extends StatefulWidget {
}
class _MainPageState extends State<MainPage> {
Future<void> _goToPage() async {
final Uri url = Uri.parse('https://google.com');
if (!await launchUrl(url, webOnlyWindowName: '_blank')) {
throw 'Could not launch $url';
String searchText = '';
List<GymItem> filteredData = [];
int cartLength = 0;
int itemViewCount = 0;
bool isLoading = false;
bool isSearching = false;
List<GymCategory> categories = [];
GymCategory? selectedCategory;
final ScrollController _scrollController = ScrollController();
final TextEditingController _searchField = TextEditingController();
@override
void initState() {
super.initState();
getCart().then((value) {
setState(() {
cartLength = value.length;
});
});
getCategories(context).then((value) => setState(() {
categories = value;
_onSearch();
}));
}
void _onLoad() async {
if (itemViewCount < filteredData.length) {
setState(() {
isLoading = true;
});
await Future.delayed(const Duration(seconds: 1));
setState(() {
itemViewCount = min(filteredData.length, itemViewCount + 5);
isLoading = false;
});
}
}
void _searchItems({String searchText = '', String categoryId = ''}) async {
setState(() {
isSearching = true;
});
final data =
await getItems(context, searchText: searchText, categoryId: categoryId);
setState(() {
filteredData = data;
itemViewCount = min(filteredData.length, 5);
});
WidgetsBinding.instance.addPostFrameCallback((_) {
for (var element in filteredData.sublist(0, itemViewCount)) {
precacheImage(NetworkImage(element.images[0].url), context);
}
});
setState(() {
isSearching = false;
});
}
void _onSearch() {
final categoryId = selectedCategory == null ? '' : selectedCategory!.id;
setState(() {
searchText = _searchField.text.trim().toLowerCase();
});
_searchItems(searchText: searchText, categoryId: categoryId);
}
@override
Widget build(BuildContext context) {
final cartL = context.watch<CartProvider>().cartLength;
return Scaffold(
appBar: widget.isLoading ? null : const GymLinkAppBar(),
body: widget.isLoading
? const Center(child: CircularProgressIndicator())
: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Search',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 8),
child: ElevatedButton(
onPressed: _goToPage,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 0,
),
minimumSize:
const Size(50, kMinInteractiveDimension),
backgroundColor:
Theme.of(context).primaryColor,
shape: const CircleBorder(),
),
child: const Icon(
Icons.search,
color: Colors.white,
size: 24,
),
),
),
),
),
),
getSpacer(context: context, flex: 2),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const BasketPage(),
));
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(0),
minimumSize: const Size(50, kMinInteractiveDimension),
backgroundColor: Theme.of(context).primaryColor,
shape: const CircleBorder(
side: BorderSide(
color: Colors.black,
width: 1,
),
),
),
child: const Icon(
Icons.shopping_basket,
color: Colors.white,
size: 24,
),
),
const SizedBox(
width: 8,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const HistoryPage(),
));
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(0),
minimumSize: const Size(50, kMinInteractiveDimension),
backgroundColor: Theme.of(context).primaryColor,
shape: const CircleBorder(
side: BorderSide(
color: Colors.black,
width: 1,
),
),
),
child: const Icon(
Icons.history,
color: Colors.white,
size: 24,
),
),
const SizedBox(
width: 10,
)
],
),
),
appBar: const GymLinkAppBar(),
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 150)
.floor(), //TODO: Make it adaptive size
),
itemCount: testData.length,
itemBuilder: (context, index) {
final product = testData[index];
return ProductCard(
imagePath: Image(
image: AssetImage('assets/${product['image']!}'),
width: 50,
),
name: product['name']!,
price: product['price']!,
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DetailPage(
name: product['name']!,
description: product['details']!,
price: product['price']!,
id: product['id']!,
image: Image(
image:
AssetImage('assets/${product['image']!}'),
width: 300),
child: TextField(
onChanged: (value) {
searchText = value.trim().toLowerCase();
if (searchText == '') {
_onSearch();
}
},
controller: _searchField,
textInputAction: TextInputAction.search,
onSubmitted: (_) => _onSearch(),
decoration: InputDecoration(
hintText: 'Поиск',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(50),
),
suffixIcon: Padding(
padding: const EdgeInsets.only(right: 5),
child: ElevatedButton(
onPressed: _onSearch,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 0,
),
minimumSize:
const Size(50, kMinInteractiveDimension),
backgroundColor: Theme.of(context).primaryColor,
shape: const CircleBorder(),
),
child: const Icon(
Icons.search,
color: Colors.white,
size: 24,
),
),
);
},
),
),
),
),
// getSpacer(context: context, flex: 2),
const SizedBox(
width: 8,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).push(CustomPageRoute(
builder: (context) => const HistoryPage(),
));
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(0),
minimumSize: const Size(50, kMinInteractiveDimension),
backgroundColor: Theme.of(context).primaryColor,
shape: const CircleBorder(
side: BorderSide(
color: Colors.black,
width: 1,
),
),
),
child: const Icon(
Icons.history,
color: Colors.white,
size: 24,
),
),
const SizedBox(
width: 10,
)
],
),
),
Align(
alignment: Alignment.centerLeft,
child: SizedBox(
height: 60,
child: ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
return GestureDetector(
onTap: () {
setState(() {
selectedCategory =
selectedCategory == category ? null : category;
});
_onSearch();
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 10),
child: Chip(
label: Text(category.name),
backgroundColor: selectedCategory == category
? Theme.of(context).primaryColor
: Colors.white,
labelStyle: TextStyle(
color: selectedCategory == category
? Colors.white
: Colors.black),
),
),
);
}),
),
),
Expanded(
child: LazyLoadScrollView(
onEndOfPage: _onLoad,
isLoading: isLoading,
child: Scrollbar(
controller: _scrollController,
child: ListView(
controller: _scrollController,
children: [
filteredData.isEmpty &&
(searchText != '' || selectedCategory != null) &&
!isSearching
? const Center(child: Text('Ничего не найдено'))
: isSearching
? const Center(child: CircularProgressIndicator())
: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: min(
(MediaQuery.sizeOf(context).width ~/
220)
.toInt(),
8),
childAspectRatio: 0.8,
mainAxisSpacing: 10.0,
crossAxisSpacing: 20.0),
itemCount: itemViewCount,
itemBuilder: (context, index) {
final product = filteredData[index];
return ProductCard(
imagePath: FutureBuilder(
future: precacheImage(
NetworkImage(product.images[0].url),
context),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
return Image(
image: NetworkImage(
product.images[0].url),
width: 120,
);
} else {
return const CircularProgressIndicator();
}
},
),
name: shortString(product.title),
price: product.price.toStringAsFixed(2),
onTap: () => Navigator.of(context).push(
CustomPageRoute(
builder: (context) => DetailPage(
id: product.id,
),
),
),
);
},
),
itemViewCount > 0 && !isSearching
? Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Center(
child: itemViewCount < filteredData.length
? !isLoading
? ElevatedButton(
onPressed: _onLoad,
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).primaryColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(50)),
),
foregroundColor: Colors.white,
fixedSize: const Size(180, 40),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('Загрузить ещё'),
Spacer(),
Icon(Icons.arrow_downward),
Spacer()
],
))
: const CircularProgressIndicator()
: const Text(
'Конец списка',
style: TextStyle(
fontSize: 10,
color: Color(0x88000000)),
),
),
)
: const SizedBox.shrink(),
],
),
),
),
),
],
),
floatingActionButton: SizedBox(
height: 80,
width: 80,
child: FittedBox(
child: Stack(
children: [
FloatingActionButton(
onPressed: () => Navigator.of(context).push(CustomPageRoute(
builder: (context) => const BasketPage(),
)),
highlightElevation: 0,
hoverColor: Colors.transparent,
focusColor: Colors.transparent,
hoverElevation: 0,
focusElevation: 0,
splashColor: Colors.transparent,
backgroundColor: Colors.transparent,
elevation: 0,
child: CircleAvatar(
radius: 25,
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Colors.white,
child: const Icon(Icons.shopping_cart_outlined)),
),
cartL > 0
? Positioned(
right: -3,
bottom: 0,
child: Card(
color: Colors.red,
child: SizedBox(
width: 20,
child: Center(
child: Text(
cartL.toString(),
style: const TextStyle(color: Colors.white),
),
),
),
),
)
: const SizedBox.shrink(),
],
),
),
),
floatingActionButtonLocation: FloatingActionButtonLocation.startFloat,
);
}
}

View File

@@ -1,9 +1,20 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:gymlink_module_web/components/app_bar.dart';
import 'package:gymlink_module_web/components/heading.dart';
import 'package:gymlink_module_web/components/order_confirm_item_card.dart';
import 'package:gymlink_module_web/interfaces/items.dart';
import 'package:gymlink_module_web/pages/order_history.dart';
import 'package:gymlink_module_web/providers/cart.dart';
import 'package:gymlink_module_web/tools/history.dart';
import 'package:gymlink_module_web/tools/items.dart';
import 'package:gymlink_module_web/tools/prefs.dart';
import 'package:gymlink_module_web/tools/routes.dart';
import 'package:gymlink_module_web/tools/text.dart';
import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
List<Map<String, dynamic>> cart = [
{
@@ -51,54 +62,198 @@ class OrderConfirmationPage extends StatefulWidget {
}
class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
List<Map<String, dynamic>> cartItems = [];
int totalPrice = 0;
List<GymItem> cartItems = [];
double totalPrice = 0;
List<GymItem> gymCart = [];
bool isAgree = false;
bool _isLoading = true;
final _emailController = TextEditingController();
final _addressController = TextEditingController();
final _nameController = TextEditingController();
Future<void> _addOrderToHistory() async {
String name = _nameController.text;
String email = _emailController.text;
String address = _addressController.text;
Set<String> supplierIdsSet = {};
for (final item in cartItems) {
supplierIdsSet.add(item.supplierId);
}
List<GymHistoryItemDetailProvider> providers = [];
for (final supplierId in supplierIdsSet) {
List<GymItem> items =
cartItems.where((e) => e.supplierId == supplierId).toList();
List<GymHistoryItemDetailItem> detailItems = [];
for (final item in items) {
detailItems.add(GymHistoryItemDetailItem(
id: item.id,
photo: item.images[0].url,
count: item.localCount,
price: item.price.toString(),
name: item.title,
));
}
GymHistoryItemDetailProvider provider = GymHistoryItemDetailProvider(
id: supplierId,
name: items.first.supplierName,
items: detailItems,
// status: 'Не оплачен'
status: Random().nextBool()
? 'Не оплачен'
: Random().nextBool()
? 'Не оплачен'
: Random().nextBool()
? 'Сборка'
: 'Доставляется',
);
providers.add(provider);
}
final order = GymHistoryItemDetail(
id: Random().nextInt(1000000).toString(),
receiver: name,
email: email,
address: address,
sum: totalPrice.toString(),
date: '',
providers: providers,
timestamp: DateTime.now().millisecondsSinceEpoch.toString(),
);
await addToHistory(order);
}
@override
void initState() {
super.initState();
getCart().then((value) {
setState(() {
cartItems = value.map((element) {
final item = cart.firstWhere((e) => e['id'] == element['id']);
return {...item, 'count': element['count'] as int};
}).toList();
totalPrice = cartItems.fold(
0,
(sum, item) =>
sum + int.parse(item['price']) * item['count'] as int);
});
});
Future.microtask(() => getCart().then((value) async {
final itemIds =
value.map((element) => element['id'] as String).toList();
final items = await getItemsByIds(context, itemIds);
setState(() {
gymCart = items;
cartItems = value.map((element) {
final item = gymCart.firstWhere((e) => e.id == element['id']);
item.localCount = element['count'] as int;
return item;
}).toList();
totalPrice = cartItems.fold(
0, (sum, item) => sum + item.price * item.localCount);
_isLoading = false;
});
}));
}
Future<void> _goToPage() async {
final Uri url = Uri.parse('https://example.org');
if (!await launchUrl(url, webOnlyWindowName: '_blank')) {
throw 'Could not launch $url';
}
}
bool _checkInputs() {
final email = _emailController.text;
final address = _addressController.text;
final name = _nameController.text;
if (!RegExp(r"^((?!\.)[\w\-_.]*[^.])(@\w+)(\.\w+(\.\w+)?[^.\W])$")
.hasMatch(email)) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Некорректный email'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('ОК'),
)
],
),
);
return false;
}
if (address.isEmpty) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('Адрес не может быть пустым'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('ОК'),
)
],
),
);
return false;
}
if (name.isEmpty) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Ошибка'),
content: const Text('ФИО не может быть пустым'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('ОК'),
)
],
),
);
return false;
}
return true;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const GymLinkAppBar(),
resizeToAvoidBottomInset: false,
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const GymLinkHeader(title: 'Оформление заказа'),
const MarkdownBody(data: '## Состав заказа:'),
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 350),
child: ListView.builder(
itemCount: cartItems.length,
itemBuilder: (context, index) {
final item = cartItems[index];
return OrderConfirmItemCard(
name: item['name'],
image: Image(
image: AssetImage('assets/${item['image']}'),
width: 50,
height: 50),
price: double.parse(item['price']),
count: item['count'],
);
},
),
),
child: _isLoading
? const Center(child: CircularProgressIndicator())
: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 350),
child: ListView.builder(
shrinkWrap: true,
itemCount: cartItems.length,
itemBuilder: (context, index) {
final item = cartItems[index];
return OrderConfirmItemCard(
name: shortString(item.title),
image: FutureBuilder(
future: precacheImage(
NetworkImage(item.images[0].url), context),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
return Image(
image: NetworkImage(item.images[0].url),
width: 50,
height: 50,
);
} else {
return const CircularProgressIndicator();
}
},
),
price: item.price,
count: item.localCount,
);
},
),
),
),
const SizedBox(
height: 10,
@@ -106,9 +261,11 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
Expanded(
child: Column(
children: [
MarkdownBody(data: '## Итого: $totalPrice'),
MarkdownBody(
data: '## Итого: ${totalPrice.toStringAsFixed(2)} руб.'),
Expanded(
child: TextField(
controller: _addressController,
decoration: InputDecoration(
hintText: 'Адрес доставки',
border: OutlineInputBorder(
@@ -119,6 +276,19 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
),
Expanded(
child: TextField(
controller: _emailController,
decoration: InputDecoration(
hintText: 'Электронная почта',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
),
keyboardType: TextInputType.emailAddress,
),
),
Expanded(
child: TextField(
controller: _nameController,
decoration: InputDecoration(
hintText: 'Получатель',
border: OutlineInputBorder(
@@ -128,17 +298,17 @@ class _OrderConfirmationPageState extends State<OrderConfirmationPage> {
),
),
ElevatedButton(
onPressed: () {
print('debugprint');
// if (kIsWeb) {
// Navigator.of(context).push(
// MaterialPageRoute(
// builder: (context) => const OrderPayPage(),
// ),
// );
// } else {
// debugPrint('test');
// }
onPressed: () async {
if (!_checkInputs()) return;
_goToPage();
await clearCart();
await _addOrderToHistory();
Provider.of<CartProvider>(context, listen: false)
.updateCartLength();
Navigator.of(context).pushAndRemoveUntil(
CustomPageRoute(
builder: (context) => const HistoryPage()),
(route) => route.isFirst);
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,

View File

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

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

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

20
lib/providers/cart.dart Normal file
View File

@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/tools/prefs.dart';
//TODO: Возможно нужно дорабатывать
class CartProvider extends ChangeNotifier {
int _cartLength = 0;
int get cartLength => _cartLength;
CartProvider() {
updateCartLength();
}
void updateCartLength() {
getCart().then((value) {
_cartLength = value.length;
notifyListeners();
});
}
}

View File

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

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/main_mobile.dart';
import 'package:gymlink_module_web/pages/main.dart';
import 'package:gymlink_module_web/providers/cart.dart';
import 'package:gymlink_module_web/providers/main.dart';
import 'package:provider/provider.dart';
@@ -8,12 +9,24 @@ class MyAppStateMobile extends State<MyApp> {
@override
Widget build(BuildContext context) {
return Consumer<GymLinkProvider>(
builder: (context, provider, __) => MaterialApp(
title: 'GymLink Module',
theme: provider.theme,
debugShowCheckedModeBanner: false,
home: MainPage(isLoading: provider.isLoading),
),
builder: (context, provider, __) {
final theme = provider.theme;
final isLoading = provider.isLoading;
return ChangeNotifierProvider(
create: (_) => CartProvider(),
builder: (context, __) => isLoading
? const Center(child: CircularProgressIndicator())
: MaterialApp(
title: 'GymLink Module',
theme: theme,
themeMode: context.read<GymLinkProvider>().blackTheme
? ThemeMode.dark
: ThemeMode.light,
debugShowCheckedModeBanner: false,
home: const MainPage(),
),
);
},
);
}
}

View File

@@ -5,12 +5,14 @@ import 'dart:js_interop_unsafe' as js_util;
import 'package:flutter/material.dart';
import 'package:gymlink_module_web/main.dart';
import 'package:gymlink_module_web/pages/main.dart';
import 'package:gymlink_module_web/providers/cart.dart';
import 'package:gymlink_module_web/providers/main.dart';
import 'package:gymlink_module_web/theme.dart';
import 'package:provider/provider.dart';
@js.JSExport()
class MyAppStateWeb extends State<MyApp> {
final _streamController = StreamController<void>.broadcast();
bool _isLoading = true;
ThemeData theme = myTheme;
bool black_theme = false;
@@ -30,28 +32,33 @@ class MyAppStateWeb extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'GymLink Module',
theme: theme,
debugShowCheckedModeBanner: false,
home: MainPage(isLoading: _isLoading),
final theme = context.watch<GymLinkProvider>().theme;
final isLoading = context.watch<GymLinkProvider>().isLoading;
return ChangeNotifierProvider(
create: (_) => CartProvider(),
child: isLoading
? const Center(child: CircularProgressIndicator())
: MaterialApp(
title: 'GymLink Module',
theme: theme,
debugShowCheckedModeBanner: false,
home: const MainPage(),
),
);
}
@js.JSExport()
void onTokenReceived(String token) {
if (token == 'token123') {
setState(() {
_isLoading = false;
});
}
void checkToken(String token) {
context.read<GymLinkProvider>().checkToken(token);
}
@js.JSExport()
void changeColor(int color) {
setState(() {
black_theme = !black_theme; //FIXME: TEMPORARY
theme = getThemeData(Color(color), black_theme);
});
void changeColor(int color, bool blackTheme) {
context.read<GymLinkProvider>().changeTheme(color, blackTheme: blackTheme);
}
@js.JSExport()
void setOnError(void Function() onError) {
context.read<GymLinkProvider>().setOnError(onError);
}
}

View File

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

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

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

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

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

8
lib/tools/routes.dart Normal file
View File

@@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
class CustomPageRoute extends MaterialPageRoute {
CustomPageRoute({builder}) : super(builder: builder);
@override
Duration get transitionDuration => const Duration(milliseconds: 0);
}

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

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

View File

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

View File

@@ -31,16 +31,16 @@ dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6
url_launcher: ^6.2.6
shared_preferences: ^2.2.3
flutter_markdown: ^0.7.1
http: ^1.2.1
universal_html: ^2.2.4
provider: ^6.1.2
lazy_load_scrollview: ^1.3.0
cupertino_icons: ^1.0.8
flutter_svg: ^2.0.10+1
carousel_slider: ^4.2.1
dev_dependencies:
flutter_test:
@@ -66,6 +66,7 @@ flutter:
assets:
- assets/logo.png
- assets/product.png
- assets/icon.svg
# To add assets to your application, add an assets section, like this:
# assets:

View File

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

View File

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

View File

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