Compare commits

..

12 Commits

Author SHA1 Message Date
e4628e977f Added tODO 2024-05-03 16:00:49 +03:00
4bbe7fbc0b Clearing shopping cart 2024-05-03 13:59:06 +03:00
5c3da0964a Added order history cards 2024-05-03 13:58:46 +03:00
26f822e83a Some fixes 2024-05-02 22:56:21 +03:00
8805b2a9a0 Basket items 2024-05-02 22:56:08 +03:00
ff29598ec5 Adding items to cart 2024-05-02 17:02:43 +03:00
727c04d368 AppBar and Header components 2024-05-02 15:04:05 +03:00
0a491ca34b Package rename 2024-05-01 20:36:40 +03:00
16d0ddca78 Goods details page 2024-05-01 20:17:20 +03:00
f941b26224 Product card on main screen 2024-05-01 16:44:18 +03:00
c6520041a6 Search btn and borders 2024-04-30 16:16:13 +03:00
7f0cef4b23 Created screens 2024-04-30 15:58:53 +03:00
22 changed files with 1268 additions and 166 deletions

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"editor.formatOnSave": true
}

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
assets/product.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

View File

@@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
class GymLinkAppBar extends StatelessWidget implements PreferredSizeWidget {
const GymLinkAppBar({super.key});
@override
Widget build(BuildContext context) {
return AppBar(
backgroundColor: Colors.white,
shadowColor: null,
automaticallyImplyLeading: false,
elevation: 0,
scrolledUnderElevation: 4,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: Image.asset('logo.png', width: 24, height: 24),
),
Align(
alignment: Alignment.centerRight,
child: Text(
'Powered by GymLink',
style: Theme.of(context).textTheme.titleSmall,
),
),
],
),
toolbarHeight: 30,
);
}
@override
Size get preferredSize => const Size.fromHeight(30);
}

View File

@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
class BasketItemCard extends StatelessWidget {
final String name;
final String price;
final String id;
final Image image;
final VoidCallback onTap;
const BasketItemCard({
super.key,
required this.name,
required this.price,
required this.id,
required this.image,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Padding(
padding:
const EdgeInsetsDirectional.symmetric(horizontal: 10, vertical: 10),
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 100,
maxHeight: 200,
minWidth: 400,
maxWidth: 600,
),
child: Card(
elevation: 4,
color: const Color(0xFFF2F3F9),
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'),
],
)
],
),
IconButton(
onPressed: onTap,
icon: const Icon(Icons.close),
)
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
class GymLinkHeader extends StatelessWidget {
final String title;
const GymLinkHeader({super.key, required this.title});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
Row(
children: [
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.arrow_back)),
Text(title, style: Theme.of(context).textTheme.titleLarge),
],
),
const Divider(thickness: 1, height: 0),
],
),
);
}
}

View File

@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
enum OrderStatus { created, inProgress, completed, canceled }
Map<OrderStatus, String> orderStatusMap = {
OrderStatus.created: 'Создан',
OrderStatus.inProgress: 'В обработке',
OrderStatus.completed: 'Завершен',
OrderStatus.canceled: 'Отменен',
};
class HistoryItemCard extends StatelessWidget {
final String id;
final String cost;
final String date;
final Image image;
final OrderStatus status;
const HistoryItemCard({
super.key,
required this.id,
required this.cost,
required this.date,
required this.status,
required this.image,
});
@override
Widget build(BuildContext context) {
return Padding(
padding:
const EdgeInsetsDirectional.symmetric(horizontal: 10, vertical: 10),
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 100,
maxHeight: 200,
minWidth: 400,
maxWidth: 600,
),
child: Card(
elevation: 4,
color: const Color(0xFFF2F3F9),
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**'),
],
)
],
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
class ProductCard extends StatelessWidget {
final Image imagePath;
final String name;
final String price;
final VoidCallback onTap;
const ProductCard({
super.key,
required this.imagePath,
required this.name,
required this.price,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 200),
child: Card(
elevation: 3,
color: const Color(0xFFF2F3F9),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
imagePath,
const SizedBox(height: 16),
Text(
name,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(
'\$$price',
style: Theme.of(context).textTheme.titleSmall,
),
],
),
),
),
);
}
}

View File

@@ -3,13 +3,18 @@ import 'dart:js_interop' as js;
import 'dart:js_interop_unsafe' as js_util;
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/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/theme.dart';
import 'package:url_launcher/url_launcher.dart';
void main() {
runApp(const MyApp());
}
enum DemoScreen { counter, textField }
class MyApp extends StatefulWidget {
const MyApp({super.key});
@@ -18,11 +23,48 @@ class MyApp extends StatefulWidget {
State<MyApp> createState() => _MyAppState();
}
const List<Map<String, String>> testData = [
{
"name": "Протеин",
"image": "product.png",
"price": "120",
"details": "Test details",
"id": "34fa3126-bfaf-5dec-8f4a-b246c097ef73"
},
{
"name": "Протеин",
"image": "product.png",
"price": "150",
"details":
"that name factory say string eaten order harbor easier said tone now floor nest it comfortable such difficulty labor bridge fact market women badly chamber heading forest allow shirt possibly story strip elephant extra even joy lungs than low discussion barn rapidly evidence is stream crew let more sold bag river triangle court slept knowledge flat package research balloon station underline careful market better make curious secret boy poor captured creature harder public tool ring subject charge planet tone scientist piece page stone support bush way feathers summer describe back should said complex song giant his that name factory say string eaten order harbor easier said tone now floor nest it comfortable such difficulty labor bridge fact market women badly chamber heading forest allow shirt possibly story strip elephant extra even joy lungs than low discussion barn rapidly evidence is stream crew let more sold bag river triangle court slept knowledge flat package research balloon station underline careful market better make curious secret boy poor captured creature harder public tool ring subject charge planet tone scientist piece page stone support bush way feathers summer describe back should said complex song giant his that name factory say string eaten order harbor easier said tone now floor nest it comfortable such difficulty labor bridge fact market women badly chamber heading forest allow shirt possibly story strip elephant extra even joy lungs than low discussion barn rapidly evidence is stream crew let more sold bag river triangle court slept knowledge flat package research balloon station underline careful market better make curious secret boy poor captured creature harder public tool ring subject charge planet tone scientist piece page stone support bush way feathers summer describe back should said complex song giant his that name factory say string eaten order harbor easier said tone now floor nest it comfortable such difficulty labor bridge fact market women badly chamber heading forest allow shirt possibly story strip elephant extra even joy lungs than low discussion barn rapidly evidence is stream crew let more sold bag river triangle court slept knowledge flat package research balloon station underline careful market better make curious secret boy poor captured creature harder public tool ring subject charge planet tone scientist piece page stone support bush way feathers summer describe back should said complex song giant his",
"id": "34a26e82-7656-5e98-a44a-c2d01d0b1ad1123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "250",
"details": "Test details",
"id": "4fb204b7-3f9e-52a2-bed1-415c00a31a37123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "300",
"details": "Test details",
"id": "09b2f5bb-683e-5c39-ae89-b8e152fa8bcf123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "100",
"details": "Test details",
"id": "cd1b6817-db94-5394-be1d-af88af79749f123"
}
];
@js.JSExport()
class _MyAppState extends State<MyApp> {
final _streanController = StreamController<void>.broadcast();
DemoScreen _currentDemoScreen = DemoScreen.counter;
int _counterScreenCount = 0;
final _streamController = StreamController<void>.broadcast();
bool _isLoading = true;
@override
@@ -35,39 +77,17 @@ class _MyAppState extends State<MyApp> {
@override
void dispose() {
_streanController.close();
_streamController.close();
super.dispose();
}
@js.JSExport()
void increment() {
if (_currentDemoScreen == DemoScreen.counter) {
setState(() {
_counterScreenCount++;
_streanController.add(null);
});
}
}
@js.JSExport()
void addHandler(void Function() handler) {
_streanController.stream.listen((event) {
handler();
});
}
@js.JSExport()
int get count => _counterScreenCount;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Aboba app',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.red),
),
title: 'GymLink Module',
theme: myTheme,
debugShowCheckedModeBanner: false,
home: demoScreenRouter(_currentDemoScreen),
home: MainPage(isLoading: _isLoading),
);
}
@@ -79,148 +99,165 @@ class _MyAppState extends State<MyApp> {
});
}
}
}
Widget demoScreenRouter(DemoScreen which) {
switch (which) {
case DemoScreen.counter:
return CounterDemo(
title: 'Counter',
numToDisplay: _counterScreenCount,
incrementHandler: increment,
isLoading: _isLoading);
case DemoScreen.textField:
return const TextFieldDemo(title: 'Nasdfs');
class MainPage extends StatefulWidget {
final bool isLoading;
const MainPage({
super.key,
required this.isLoading,
});
@override
State<MainPage> createState() => _MainPageState();
}
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';
}
}
@js.JSExport()
void changeDemoScreenTo(String screenString) {
setState(() {
switch (screenString) {
case 'counter':
_currentDemoScreen = DemoScreen.counter;
break;
case 'textField':
_currentDemoScreen = DemoScreen.textField;
break;
}
});
}
}
class CounterDemo extends StatefulWidget {
final String title;
final int numToDisplay;
final VoidCallback incrementHandler;
final bool isLoading;
const CounterDemo(
{super.key,
required this.title,
required this.numToDisplay,
required this.incrementHandler,
required this.isLoading});
@override
State<CounterDemo> createState() => _CounterDemoState();
}
class _CounterDemoState extends State<CounterDemo> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: widget.isLoading
? null
: AppBar(
title: Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: 'Search',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
suffixIcon: const Icon(
Icons.search,
color: Colors.blue,
),
),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(0),
shape: const CircleBorder(
side: BorderSide(
color: Colors.blue,
width: 2,
),
),
),
child: const Icon(
Icons.shopping_basket,
color: Colors.white,
size: 36,
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(0),
shape: const CircleBorder(
side: BorderSide(
color: Colors.blue,
width: 2,
),
),
),
child: const Icon(
Icons.history,
color: Colors.white,
size: 36,
),
),
],
),
),
appBar: widget.isLoading ? null : const GymLinkAppBar(),
body: widget.isLoading
? const Center(child: CircularProgressIndicator())
: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Здесь будут товары',
: 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,
),
),
),
),
),
),
const Spacer(
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,
)
],
),
],
),
),
Expanded(
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount:
(MediaQuery.sizeOf(context).width ~/ 250).floor(),
),
itemCount: testData.length,
itemBuilder: (context, index) {
final product = testData[index];
return ProductCard(
imagePath: Image.asset(
product['image']!,
width: 100,
),
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.asset(product['image']!, width: 300),
),
),
),
);
},
),
),
],
),
);
}
}
class TextFieldDemo extends StatelessWidget {
const TextFieldDemo({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: const Center(
child: Padding(
padding: EdgeInsets.all(14.0),
child: TextField(
maxLines: null,
decoration: InputDecoration(border: OutlineInputBorder()),
),
),
),
);
}
}

249
lib/pages/basket.dart Normal file
View File

@@ -0,0 +1,249 @@
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/tools/prefs.dart';
List<Map<String, dynamic>> cart = [
{
"name": "Протеин",
"image": "product.png",
"price": "120",
"details": "Test details",
"id": "34fa3126-bfaf-5dec-8f4a-b246c097ef73"
},
{
"name": "Протеин",
"image": "product.png",
"price": "150",
"details": "Test details",
"id": "34a26e82-7656-5e98-a44a-c2d01d0b1ad1123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "250",
"details": "Test details",
"id": "4fb204b7-3f9e-52a2-bed1-415c00a31a37123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "300",
"details": "Test details",
"id": "09b2f5bb-683e-5c39-ae89-b8e152fa8bcf123"
},
{
"name": "Протеин",
"image": "product.png",
"price": "100",
"details": "Test details",
"id": "cd1b6817-db94-5394-be1d-af88af79749f123"
}
];
//TODO: Сделать правильное удаление из корзины по одному
class BasketPage extends StatefulWidget {
const BasketPage({super.key});
@override
State<BasketPage> createState() => _BasketPageState();
}
class _BasketPageState extends State<BasketPage> {
List<Map<String, dynamic>> cartItems = [];
int totalPrice = 0;
@override
void initState() {
super.initState();
getCart().then((value) {
setState(() {
cartItems = cart
.where((element) => value.any((e) => e['id'] == element['id']))
.toList();
totalPrice =
cartItems.fold(0, (sum, item) => sum + int.parse(item['price']));
});
});
}
void removeItem(String id) async {
setState(() {
cartItems.removeWhere((element) => element['id'] == id);
totalPrice =
cartItems.fold(0, (sum, item) => sum + int.parse(item['price']));
});
await removeItemFromCart(id);
}
Future<void> _deleteItemAlert(String id, String name) async {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Удаление из корзины'),
content: SingleChildScrollView(
child: ListBody(
children: [
Text('Вы действительно хотите убрать "$name" из корзины?'),
],
),
),
actions: [
TextButton(
child: const Text('Отмена'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Удалить'),
onPressed: () {
removeItem(id);
Navigator.of(context).pop();
},
),
],
);
},
);
}
Future<void> _clearCartAlert() async {
return showDialog<void>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Очистка корзины'),
content: const SingleChildScrollView(
child: ListBody(
children: [
Text('Вы действительно хотите очистить корзину?'),
],
),
),
actions: [
TextButton(
child: const Text('Отмена'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Очистить'),
onPressed: () {
clearCart();
setState(() {
cartItems = [];
});
Navigator.of(context).pop();
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const GymLinkAppBar(),
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: Row(
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.asset(
item['image'],
width: 50,
),
onTap: () =>
_deleteItemAlert(item['id'], item['name']),
);
},
),
),
const Spacer(),
Padding(
padding: const EdgeInsetsDirectional.symmetric(
horizontal: 10, vertical: 10),
child: Column(
children: [
Text(
'Итого: $totalPrice',
),
ElevatedButton(
onPressed: () {},
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),
],
),
),
],
),
);
}
}

173
lib/pages/detail.dart Normal file
View File

@@ -0,0 +1,173 @@
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/tools/prefs.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
State<DetailPage> createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
bool isInCart = false;
int quantity = 0;
@override
void initState() {
super.initState();
getCart().then((value) {
setState(() {
isInCart = value.any((element) => element['id'] == widget.id);
if (isInCart) {
quantity = value
.firstWhere((element) => element['id'] == widget.id)['count'];
}
});
});
}
Widget _buildButton() {
if (!isInCart) {
return ElevatedButton(
onPressed: () async {
await addItemToCart(widget.id);
setState(() {
isInCart = true;
quantity = 1;
});
},
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(50)),
),
foregroundColor: Colors.white,
padding: const EdgeInsetsDirectional.fromSTEB(34, 10, 34, 10)),
child: const Text('Добавить в корзину'),
);
} else {
return 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;
}
});
},
),
Text('$quantity'),
IconButton(
icon: const Icon(Icons.add),
onPressed: () async {
await addItemToCart(widget.id);
setState(() {
quantity++;
});
},
),
],
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const GymLinkAppBar(),
body: Column(mainAxisAlignment: MainAxisAlignment.start, children: [
GymLinkHeader(title: '${widget.name} - ${widget.id}'),
Expanded(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(20),
child: SizedBox(
width: MediaQuery.sizeOf(context).width,
height: MediaQuery.sizeOf(context).height,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
widget.image,
Padding(
padding:
const EdgeInsetsDirectional.fromSTEB(0, 30, 60, 60),
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 340,
maxWidth: 340,
maxHeight: 600,
),
child: Card(
elevation: 4,
color: const Color(0xFFF2F3F9),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsetsDirectional.fromSTEB(
20, 15, 10, 15),
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: 100,
),
child: Text(
widget.description,
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
),
),
),
),
Align(
alignment: const AlignmentDirectional(0, -1),
child: Padding(
padding:
const EdgeInsetsDirectional.fromSTEB(0, 60, 0, 0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Стоимость ${widget.price}',
style: Theme.of(context).textTheme.bodyLarge,
),
_buildButton()
],
),
),
),
],
),
),
),
),
),
]),
);
}
}

View File

@@ -0,0 +1,80 @@
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';
List<Map<String, String>> orders = [
{
"image": "product.png",
"price": "120",
"id": "34fa3126-bfaf-5dec-8f4a-b246c097ef73",
"date": "11.09.2001"
},
{
"image": "product.png",
"price": "150",
"id": "34a26e82-7656-5e98-a44a-c2d01d0b1ad1123",
"date": "11.09.2001",
},
{
"image": "product.png",
"price": "250",
"id": "4fb204b7-3f9e-52a2-bed1-415c00a31a37123",
"date": "11.09.2001",
},
{
"image": "product.png",
"price": "300",
"id": "09b2f5bb-683e-5c39-ae89-b8e152fa8bcf123",
"date": "11.09.2001",
},
{
"image": "product.png",
"price": "100",
"id": "cd1b6817-db94-5394-be1d-af88af79749f123",
"date": "11.09.2001",
}
];
class HistoryPage extends StatelessWidget {
const HistoryPage({
super.key,
});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: const GymLinkAppBar(),
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.asset(
item['image']!,
width: 50,
),
status: OrderStatus.completed,
);
},
),
),
const Spacer(),
],
),
),
],
),
);
}
}

26
lib/theme.dart Normal file
View File

@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
final ThemeData myTheme = ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: getMaterialColor(const Color(0x007d85ff))));
MaterialColor getMaterialColor(Color color) {
final int red = color.red;
final int green = color.green;
final int blue = color.blue;
final Map<int, Color> shades = {
50: Color.fromRGBO(red, green, blue, .1),
100: Color.fromRGBO(red, green, blue, .2),
200: Color.fromRGBO(red, green, blue, .3),
300: Color.fromRGBO(red, green, blue, .4),
400: Color.fromRGBO(red, green, blue, .5),
500: Color.fromRGBO(red, green, blue, .6),
600: Color.fromRGBO(red, green, blue, .7),
700: Color.fromRGBO(red, green, blue, .8),
800: Color.fromRGBO(red, green, blue, .9),
900: Color.fromRGBO(red, green, blue, 1),
};
return MaterialColor(color.value, shades);
}

47
lib/tools/prefs.dart Normal file
View File

@@ -0,0 +1,47 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
Future<void> addItemToCart(String id) async {
final prefs = await SharedPreferences.getInstance();
String cartString = prefs.getString('cart') ?? "[]";
List<Map<String, dynamic>> cart =
List<Map<String, dynamic>>.from(jsonDecode(cartString) as List<dynamic>);
final index = cart.indexWhere((element) => element['id'] == id);
if (index == -1) {
cart.add({'id': id, 'count': 1});
} else {
cart[index]['count'] = cart[index]['count']! + 1;
}
prefs.setString('cart', jsonEncode(cart));
}
Future<List<Map<String, dynamic>>> getCart() async {
final prefs = await SharedPreferences.getInstance();
String cartString = prefs.getString('cart') ?? "[]";
List<Map<String, dynamic>> cart =
List<Map<String, dynamic>>.from(jsonDecode(cartString) as List<dynamic>);
return cart;
}
Future<void> removeItemFromCart(String id) async {
final prefs = await SharedPreferences.getInstance();
String cartString = prefs.getString('cart') ?? "[]";
List<Map<String, dynamic>> cart =
List<Map<String, dynamic>>.from(jsonDecode(cartString) as List<dynamic>);
cart.removeWhere((element) => element['id'] == id && element['count'] == 1);
for (final item in cart) {
if (item['id'] == id) {
item['count'] = item['count']! - 1;
if (item['count'] == 0) {
cart.remove(item);
}
}
}
prefs.setString('cart', jsonEncode(cart));
}
Future<void> clearCart() async {
final prefs = await SharedPreferences.getInstance();
prefs.setString('cart', "[]");
}

View File

@@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
}

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@@ -5,6 +5,10 @@
import FlutterMacOS
import Foundation
import shared_preferences_foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
}

View File

@@ -57,6 +57,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
file:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
flutter:
dependency: "direct main"
description: flutter
@@ -75,6 +91,11 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
@@ -139,6 +160,102 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
platform:
dependency: transitive
description:
name: platform
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
url: "https://pub.dev"
source: hosted
version: "3.1.4"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180
url: "https://pub.dev"
source: hosted
version: "2.2.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
url: "https://pub.dev"
source: hosted
version: "2.3.5"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a"
url: "https://pub.dev"
source: hosted
version: "2.3.0"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
sky_engine:
dependency: transitive
description: flutter
@@ -192,6 +309,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.1"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e"
url: "https://pub.dev"
source: hosted
version: "6.2.6"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775"
url: "https://pub.dev"
source: hosted
version: "6.3.1"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5"
url: "https://pub.dev"
source: hosted
version: "6.2.5"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
url: "https://pub.dev"
source: hosted
version: "3.1.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
url: "https://pub.dev"
source: hosted
version: "3.1.0"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
url: "https://pub.dev"
source: hosted
version: "3.1.1"
vector_math:
dependency: transitive
description:
@@ -208,5 +389,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "13.0.0"
web:
dependency: transitive
description:
name: web
sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
win32:
dependency: transitive
description:
name: win32
sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb"
url: "https://pub.dev"
source: hosted
version: "5.5.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev"
source: hosted
version: "1.0.4"
sdks:
dart: ">=3.3.3 <4.0.0"
flutter: ">=3.19.0"

View File

@@ -1,5 +1,5 @@
name: flutter_application_1
description: "A new Flutter project."
name: gymlink_module_web
description: "GymLink Flutter Web Module."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
@@ -35,6 +35,9 @@ dependencies:
# 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
dev_dependencies:
flutter_test:
@@ -57,6 +60,9 @@ flutter:
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
assets:
- assets/logo.png
- assets/product.png
# To add assets to your application, add an assets section, like this:
# assets:

View File

@@ -1,7 +1,7 @@
#flutter_target {
border: 1px solid #aaa;
width: 320px;
height: 480px;
width: 80vw;
height: 60vh;
border-radius: 0px;
transition: all 150ms ease-in;
align-self: center;

View File

@@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h"
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}

View File

@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST