Merge pull request #11 from Shchoholiev/feature/SA-178-product-selection

Feature/sa 178 product selection
This commit is contained in:
Serhii Shchoholiev 2023-12-16 15:35:45 -08:00 committed by GitHub
commit 9b4a80efe4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 438 additions and 52 deletions

View File

@ -0,0 +1,3 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 15H26M15 25V5" stroke="#009FFF" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 215 B

4
assets/icons/back.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.1008 25.9391H21.4342C25.3442 25.9391 28.5175 22.7658 28.5175 18.8558C28.5175 14.9458 25.3442 11.7725 21.4342 11.7725H5.85083" stroke="#202124" stroke-width="2.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.10909 15.3141L5.48242 11.6875L9.10909 8.06079" stroke="#202124" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 494 B

View File

@ -0,0 +1,10 @@
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_295_534)">
<path d="M10.8696 2.77884L12.263 5.5655C12.453 5.95342 12.9596 6.3255 13.3871 6.39675L15.9126 6.81634C17.5276 7.0855 17.9075 8.25717 16.7438 9.413L14.7805 11.3763C14.448 11.7088 14.2659 12.3501 14.3688 12.8093L14.9309 15.2397C15.3742 17.1634 14.353 17.9076 12.6509 16.9022L10.2838 15.5009C9.8563 15.2476 9.15172 15.2476 8.7163 15.5009L6.34922 16.9022C4.65505 17.9076 3.62588 17.1555 4.06922 15.2397L4.6313 12.8093C4.73422 12.3501 4.55213 11.7088 4.21963 11.3763L2.2563 9.413C1.10047 8.25717 1.47255 7.0855 3.08755 6.81634L5.61297 6.39675C6.03255 6.3255 6.53922 5.95342 6.72922 5.5655L8.12255 2.77884C8.88255 1.26675 10.1176 1.26675 10.8696 2.77884Z" stroke="#FFC700" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_295_534">
<rect width="19" height="19" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 980 B

View File

@ -0,0 +1,3 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 18.75L18.75 15M18.75 15L15 11.25M18.75 15H5M5 9.06V9C5 7.6 5 6.9 5.2725 6.365C5.5125 5.89375 5.89375 5.5125 6.365 5.2725C6.9 5 7.6 5 9 5H21C22.4 5 23.1 5 23.6337 5.2725C24.105 5.5125 24.4875 5.89375 24.7275 6.365C25 6.89875 25 7.59875 25 8.99625V21.005C25 22.4025 25 23.1013 24.7275 23.635C24.4874 24.1055 24.1045 24.4879 23.6337 24.7275C23.1 25 22.4012 25 21.0037 25H8.99625C7.59875 25 6.89875 25 6.365 24.7275C5.89462 24.4878 5.51218 24.1054 5.2725 23.635C5 23.1 5 22.4 5 21V20.9375" stroke="#0165FF" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 687 B

View File

@ -0,0 +1,9 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.47409 1.91065L8.43216 3.82679C8.56281 4.09353 8.9112 4.34937 9.20515 4.39837L10.9417 4.68688C12.0521 4.87196 12.3134 5.67761 11.5132 6.47237L10.1632 7.82238C9.93459 8.05101 9.80939 8.49194 9.88015 8.80766L10.2666 10.4788C10.5715 11.8016 9.86927 12.3133 8.6989 11.622L7.07127 10.6585C6.77731 10.4843 6.29284 10.4843 5.99344 10.6585L4.36581 11.622C3.20088 12.3133 2.49322 11.7962 2.79806 10.4788L3.18455 8.80766C3.25532 8.49194 3.13012 8.05101 2.90149 7.82238L1.55148 6.47237C0.756716 5.67761 1.01256 4.87196 2.12305 4.68688L3.85956 4.39837C4.14807 4.34937 4.49645 4.09353 4.6271 3.82679L5.58517 1.91065C6.10775 0.870929 6.95695 0.870929 7.47409 1.91065Z" fill="url(#paint0_linear_554_62)" stroke="#FFC700" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<defs>
<linearGradient id="paint0_linear_554_62" x1="1" y1="6" x2="12" y2="6" gradientUnits="userSpaceOnUse">
<stop offset="0.53125" stop-color="#FFC700"/>
<stop offset="0.557292" stop-color="#FFC700" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

3
assets/icons/heart.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="28" height="26" viewBox="0 0 28 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.775 24.0125C14.35 24.1625 13.65 24.1625 13.225 24.0125C9.6 22.775 1.5 17.6125 1.5 8.8625C1.5 5 4.6125 1.875 8.45 1.875C10.725 1.875 12.7375 2.975 14 4.675C14.6422 3.80734 15.4787 3.10216 16.4425 2.61593C17.4063 2.1297 18.4705 1.87595 19.55 1.875C23.3875 1.875 26.5 5 26.5 8.8625C26.5 17.6125 18.4 22.775 14.775 24.0125Z" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 520 B

10
assets/icons/star.svg Normal file
View File

@ -0,0 +1,10 @@
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_295_532)">
<path d="M10.8696 2.77884L12.263 5.56551C12.453 5.95342 12.9596 6.3255 13.3871 6.39675L15.9125 6.81634C17.5276 7.0855 17.9075 8.25717 16.7438 9.413L14.7805 11.3763C14.448 11.7088 14.2659 12.3501 14.3688 12.8093L14.9309 15.2397C15.3742 17.1634 14.353 17.9076 12.6509 16.9022L10.2838 15.5009C9.8563 15.2476 9.15172 15.2476 8.7163 15.5009L6.34922 16.9022C4.65505 17.9076 3.62588 17.1555 4.06922 15.2397L4.6313 12.8093C4.73422 12.3501 4.55213 11.7088 4.21963 11.3763L2.2563 9.413C1.10047 8.25717 1.47255 7.0855 3.08755 6.81634L5.61297 6.39675C6.03255 6.3255 6.53922 5.95342 6.72922 5.56551L8.12255 2.77884C8.88255 1.26675 10.1175 1.26675 10.8696 2.77884Z" fill="#FFC700" stroke="#FFC700" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_295_532">
<rect width="19" height="19" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 997 B

3
assets/icons/x.svg Normal file
View File

@ -0,0 +1,3 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.4324 22.4425C24.6966 22.7067 24.845 23.065 24.845 23.4386C24.845 23.8122 24.6966 24.1705 24.4324 24.4347C24.1682 24.6988 23.8099 24.8473 23.4363 24.8473C23.0627 24.8473 22.7044 24.6988 22.4402 24.4347L14.9999 16.9921L7.55737 24.4323C7.29319 24.6965 6.93488 24.8449 6.56128 24.8449C6.18767 24.8449 5.82936 24.6965 5.56518 24.4323C5.301 24.1681 5.15259 23.8098 5.15259 23.4362C5.15259 23.0626 5.301 22.7043 5.56518 22.4401L13.0078 14.9999L5.56753 7.55732C5.30335 7.29314 5.15493 6.93484 5.15493 6.56123C5.15493 6.18762 5.30335 5.82932 5.56753 5.56513C5.83171 5.30095 6.19001 5.15254 6.56362 5.15254C6.93723 5.15254 7.29553 5.30095 7.55971 5.56513L14.9999 13.0077L22.4425 5.56396C22.7067 5.29978 23.065 5.15137 23.4386 5.15137C23.8122 5.15137 24.1705 5.29978 24.4347 5.56396C24.6989 5.82814 24.8473 6.18645 24.8473 6.56006C24.8473 6.93366 24.6989 7.29197 24.4347 7.55615L16.9921 14.9999L24.4324 22.4425Z" fill="#0165FF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,3 +1,5 @@
import 'dart:ffi';
class Product {
Product({
required this.id,
@ -5,8 +7,10 @@ class Product {
required this.url,
required this.imageUrls,
required this.rating,
required this.price
});
required this.price,
required this.description,
required this.wasOpened,
});
String id;
String name;
@ -14,6 +18,8 @@ class Product {
List<String> imageUrls;
double rating;
double price;
String description;
bool wasOpened;
Product.fromJson(Map<String, dynamic> json)
: id = json['id'] as String,
@ -21,5 +27,7 @@ class Product {
url = json['url'] as String,
imageUrls = json['imageUrls'] as List<String>,
rating = json['rating'] as double,
price = json['name'] as double;
price = json['name'] as double,
description = json['description'] as String,
wasOpened = json['wasOpened'] as bool;
}

View File

@ -0,0 +1,31 @@
import 'package:graphql/client.dart';
import 'package:shopping_assistant_mobile_client/models/product.dart';
import 'package:shopping_assistant_mobile_client/network/api_client.dart';
class ProductService {
final ApiClient client = ApiClient();
Future<void> addProductToPersonalWishlist(Product product, String wishlisId) async {
final options = MutationOptions(
document: gql('''
mutation AddProductToPersonalWishlist(\$wishlistId: String!, \$dto: ProductCreateDtoInput!) {
addProductToPersonalWishlist(wishlistId: \$wishlistId, dto: \$dto) {
}
}
'''),
variables: {'wishlistId': wishlisId,
'dto': {
'wasOpened': product.wasOpened,
'url': product.url,
'rating': product.rating,
'price': product.price,
'name': product.name,
'imagesUrls': product.imageUrls,
'description': product.description,
}},
);
final result = await client.mutate(options);
}
}

View File

@ -22,6 +22,8 @@ SearchEventType type = SearchEventType.message;
class SearchService {
final ApiClient client = ApiClient();
List<String> products = [];
late final _sseController = StreamController<ServerSentEvent>();
Stream<ServerSentEvent> get sseStream => _sseController.stream;
@ -92,6 +94,11 @@ class SearchService {
final event = ServerSentEvent(chunk.event, cleanedMessage);
type = chunk.event;
if(type == SearchEventType.product) {
String pattern = r'[\\\"]';
String product = event.data.replaceAll(RegExp(pattern), '');
products.add(product);
}
_sseController.add(event);
}
}

287
lib/screens/cards.dart Normal file
View File

@ -0,0 +1,287 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:shopping_assistant_mobile_client/models/product.dart';
import 'package:shopping_assistant_mobile_client/network/product_service.dart';
class Cards extends StatefulWidget {
final String wishlistId;
final String wishlistName;
List<String> inputProducts = [];
List<Product> products = [];
Cards({required this.wishlistName, required this.inputProducts, required this.wishlistId});
@override
_CardsState createState() => _CardsState();
}
class _CardsState extends State<Cards> {
int currentProduct = 0;
ProductService productService = ProductService();
List<Product> cart = [];
@override
void initState(){
for(String productName in this.widget.inputProducts){
this.widget.products.add(
Product(
id: '',
name: productName,
url: 'link',
imageUrls: [],
rating: 0.0,
price: 0.0,
description: '',
wasOpened: false,
)
);
}
}
Widget buildRatingStars(double rating) {
int whole = rating.floor();
double fractal = rating - whole;
List<Widget> stars = [];
for (int i = 0; i < 5; i++) {
if (i < whole) {
// Whole star
stars.add(SvgPicture.asset(
'assets/icons/star.svg',
width: 19,
height: 19,
));
} else if (fractal != 0.0) {
// Half star
stars.add(SvgPicture.asset(
'assets/icons/half-star.svg',
width: 19,
height: 19,
));
fractal -= fractal;
} else {
// Empty star
stars.add(SvgPicture.asset(
'assets/icons/empty-star.svg',
width: 19,
height: 19,
));
}
}
return Row(
children: stars,
);
}
@override
Widget build(BuildContext context) {
bool hasCards = widget.products.isNotEmpty;
bool isLastProduct = currentProduct == widget.products.length;
return Scaffold(
appBar: AppBar(
title: Text(widget.wishlistName),
centerTitle: true,
),
body: Center(
child: Stack(
children: [
if (currentProduct < widget.products.length - 1)
Positioned(
child: Transform.rotate(
angle: -5 * 3.1415926535 / 180,
child: Container(
width: 300,
height: 600,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.white,
border: Border.all(
color: Color(0xFF009FFF),
),
boxShadow: [
BoxShadow(
color: Color(0xFF0165FF),
blurRadius: 8.0,
spreadRadius: 0.0,
offset: Offset(0, 0),
),
],
),
),
),
),
Positioned(
child: Container(
width: 300,
height: 600,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.white,
border: Border.all(
color: Color(0xFF009FFF),
),
boxShadow: [
BoxShadow(
color: Color(0xFF0165FF),
blurRadius: 8.0,
spreadRadius: 0.0,
offset: Offset(0, 0),
),
],
),
child: Column(
children: [
Container(
height: 300,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/product_image_placeholder.jpg'), // Replace with your image path
fit: BoxFit.cover,
),
),
),
SizedBox(height: 10),
Text(
currentProduct < widget.products.length
? widget.products[currentProduct].name
: "The cards ended.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
if (currentProduct == widget.products.length)
Text(
"Swipe right to show more or left to exit.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 10),
if (!isLastProduct)
Column(
children: [
Text(
currentProduct < widget.products.length
? widget.products[currentProduct].description
: "Product description not available.",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w400,
),
),
SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
buildRatingStars(currentProduct < widget.products.length
? widget.products[currentProduct].rating
: 0.0),
SizedBox(width: 20),
Text(
'\$${currentProduct < widget.products.length ? widget.products[currentProduct].price : "-"}',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
SizedBox(height: 50),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: currentProduct < widget.products.length
? SvgPicture.asset(
'assets/icons/x.svg',
width: 30,
height: 30,
)
: SvgPicture.asset(
'assets/icons/exit-cards.svg',
width: 30,
height: 30,
),
onPressed: () {
if (currentProduct < widget.products.length) {
showNextProduct();
} else {
Navigator.of(context).pop();
}
},
),
IconButton(
icon: SvgPicture.asset(
'assets/icons/back.svg',
width: 30,
height: 30,
),
onPressed: () {
showPreviousProduct();
},
),
IconButton(
icon: currentProduct < widget.products.length
? SvgPicture.asset(
'assets/icons/heart.svg',
width: 30,
height: 30,
color: Colors.blue,
)
: SvgPicture.asset(
'assets/icons/add-products.svg',
width: 30,
height: 30,
),
onPressed: () async {
if (currentProduct < widget.products.length) {
if (!cart.contains(widget.products[currentProduct])) {
await productService.addProductToPersonalWishlist(
widget.products[currentProduct],
widget.wishlistId,
);
cart.add(widget.products[currentProduct]);
}
showNextProduct();
}
},
),
],
),
],
),
),
),
],
),
),
);
}
void showNextProduct() {
setState(() {
if(currentProduct < widget.products.length) {
++currentProduct;
}
});
}
void showPreviousProduct() {
setState(() {
if(currentProduct > 0 ) {
--currentProduct;
}
});
}
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:logger/logger.dart';
import 'package:shopping_assistant_mobile_client/network/search_service.dart';
import 'package:shopping_assistant_mobile_client/screens/cards.dart';
class Message {
final String text;
@ -39,22 +40,11 @@ class MessageBubble extends StatelessWidget {
children: [
Text(
message.trim(),
style: TextStyle(color: isOutgoing ? Colors.white : Colors.black,
fontSize: 18.0
style: TextStyle(
color: isOutgoing ? Colors.white : Colors.black,
fontSize: 18.0,
),
),
if (isProduct)
ElevatedButton(
onPressed: () {
print('View Product button pressed');
},
style: ElevatedButton.styleFrom(
primary: Colors.indigo,
onPrimary: Colors.white,
minimumSize: Size(300, 50)
),
child: Text('View Product'),
),
],
),
),
@ -184,6 +174,7 @@ class ChatScreenState extends State<ChatScreen> {
if (wishlistName != null) {
setState(() {
appBarTitle = Text(wishlistName, style: TextStyle(fontSize: 18.0));
this.widget.wishlistName = wishlistName;
});
}
}
@ -214,6 +205,15 @@ class ChatScreenState extends State<ChatScreen> {
_scrollToBottom();
setState(() {
if(_searchService.checkerForProduct()){
messages.removeLast();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => Cards(wishlistName: this.widget.wishlistName, inputProducts: this._searchService.products, wishlistId: this.widget.wishlistId,),
),
);
}
isWaitingForResponse = false;
});
}

View File

@ -244,10 +244,10 @@ packages:
dependency: "direct main"
description:
name: http
sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525"
sha256: d4872660c46d929f6b8a9ef4e7a7eff7e49bbf0c4ec3f385ee32df5119175139
url: "https://pub.dev"
source: hosted
version: "1.1.0"
version: "1.1.2"
http_parser:
dependency: transitive
description:
@ -396,10 +396,10 @@ packages:
dependency: transitive
description:
name: petitparser
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "5.4.0"
version: "6.0.2"
platform:
dependency: transitive
description:
@ -468,10 +468,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_web
sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.2.2"
shared_preferences_windows:
dependency: transitive
description:
@ -569,10 +569,10 @@ packages:
dependency: transitive
description:
name: url_launcher_linux
sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd"
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.1"
url_launcher_macos:
dependency: transitive
description:
@ -593,18 +593,18 @@ packages:
dependency: transitive
description:
name: url_launcher_web
sha256: "7fd2f55fe86cea2897b963e864dc01a7eb0719ecc65fcef4c1cc3d686d718bb2"
sha256: "7286aec002c8feecc338cc33269e96b73955ab227456e9fb2a91f7fab8a358e9"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.2.2"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc"
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
url: "https://pub.dev"
source: hosted
version: "3.1.0"
version: "3.1.1"
uuid:
dependency: "direct main"
description:
@ -681,10 +681,10 @@ packages:
dependency: transitive
description:
name: xml
sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84"
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.5.0"
yaml:
dependency: transitive
description:
@ -694,5 +694,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.2.0-194.0.dev <4.0.0"
flutter: ">=3.13.0"
dart: ">=3.2.0 <4.0.0"
flutter: ">=3.16.0"

View File

@ -74,6 +74,14 @@ flutter:
- assets/icons/wishlists.svg
- assets/icons/start-new-search.svg
- assets/icons/settings.svg
- assets/icons/heart.svg
- assets/icons/x.svg
- assets/icons/exit-cards.svg
- assets/icons/back.svg
- assets/icons/add-products.svg
- assets/icons/star.svg
- assets/icons/half-star.svg
- assets/icons/empty-star.svg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware