diff --git a/API/Application/Users/Queries/QueryUsersByIds.cs b/API/Application/Users/Queries/QueryUsersByIds.cs new file mode 100644 index 0000000..034f56d --- /dev/null +++ b/API/Application/Users/Queries/QueryUsersByIds.cs @@ -0,0 +1,32 @@ +using API.Models; +using API.Persistence.Repositories; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages.Manage; +using Newtonsoft.Json.Linq; + +namespace API.Application.Users.Queries +{ + public class QueryUsersByIds + { + private readonly IUserRepository _repository; + + public QueryUsersByIds(IUserRepository repository) + { + _repository = repository; + } + + public async Task>> Handle(List ids) + { + List users = await _repository.QueryUsersByIdsAsync(ids); + + if (users == null) + { + return new ConflictObjectResult(new { message = "No user on given Id" }); + } + + return new OkObjectResult(users.Select(user => new { id = user.Id, email = user.Email, username = user.Username, profilePicture = user.ProfilePicture, createdAt = user.CreatedAt })); + + } + + } +} diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index 985d255..14f50b7 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -22,36 +22,35 @@ namespace API.Controllers { private readonly QueryAllUsers _queryAllUsers; private readonly QueryUserById _queryUserById; + private readonly QueryUsersByIds _queryUsersByIds; private readonly CreateUser _createUser; private readonly UpdateUser _updateUser; private readonly DeleteUser _deleteUser; private readonly LoginUser _loginUser; private readonly TokenHelper _tokenHelper; - - private readonly IUserRepository _repository; public UsersController( QueryAllUsers queryAllUsers, QueryUserById queryUserById, + QueryUsersByIds queryUsersByIds, CreateUser createUser, UpdateUser updateUser, DeleteUser deleteUser, LoginUser loginUser, TokenHelper tokenHelper, IUserRepository repository - ) + ) { _queryAllUsers = queryAllUsers; _queryUserById = queryUserById; + _queryUsersByIds = queryUsersByIds; _createUser = createUser; _updateUser = updateUser; _deleteUser = deleteUser; _loginUser = loginUser; _tokenHelper = tokenHelper; _repository = repository; - - } [HttpPost("login")] @@ -73,9 +72,16 @@ namespace API.Controllers return await _queryUserById.Handle(id); } + [HttpGet("UsersByIds")] + public async Task>> GetUsersByIds(string userIds) + { + List ids = userIds.Split(",").ToList(); + return await _queryUsersByIds.Handle(ids); + } + [Authorize] [HttpPut] - public async Task PutUser([FromForm ]UpdateUserDTO UpdateUserDTO) + public async Task PutUser([FromForm] UpdateUserDTO UpdateUserDTO) { return await _updateUser.Handle(UpdateUserDTO); } diff --git a/API/Persistence/Repositories/IUserRepository.cs b/API/Persistence/Repositories/IUserRepository.cs index 9f2584d..737fb2d 100644 --- a/API/Persistence/Repositories/IUserRepository.cs +++ b/API/Persistence/Repositories/IUserRepository.cs @@ -8,6 +8,7 @@ namespace API.Persistence.Repositories Task DeleteUserAsync(string id); Task> QueryAllUsersAsync(); Task QueryUserByIdAsync(string id); + Task> QueryUsersByIdsAsync(List ids); Task QueryUserByEmailAsync(string email); Task UpdateUserAsync(User user); Task QueryUserByRefreshTokenAsync(string refreshToken); diff --git a/API/Persistence/Repositories/UserRepository.cs b/API/Persistence/Repositories/UserRepository.cs index e007973..12a04c0 100644 --- a/API/Persistence/Repositories/UserRepository.cs +++ b/API/Persistence/Repositories/UserRepository.cs @@ -1,5 +1,6 @@ using API.Models; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages; namespace API.Persistence.Repositories @@ -25,6 +26,18 @@ namespace API.Persistence.Repositories } } + public async Task> QueryUsersByIdsAsync(List ids) + { + try + { + return _context.Users.Where(user => ids.Contains(user.Id)).ToList(); + } + catch (Exception) + { + return []; + } + } + public async Task CreateUserAsync(User user) { try diff --git a/API/Program.cs b/API/Program.cs index 522abfe..a5cd0be 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -43,7 +43,7 @@ namespace API builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); - + builder.Services.AddScoped(); IConfiguration Configuration = builder.Configuration; diff --git a/Mobile/lib/api.dart b/Mobile/lib/api.dart index 7dc969f..3ee3e61 100644 --- a/Mobile/lib/api.dart +++ b/Mobile/lib/api.dart @@ -13,6 +13,8 @@ enum ApiService { } Future request(BuildContext? context, ApiService service, String method, String path, dynamic body) async { + var debug = '$method $path\n $body\n'; + final messenger = context != null ? ScaffoldMessenger.of(context) : null; final prefs = await SharedPreferences.getInstance(); @@ -47,18 +49,22 @@ Future request(BuildContext? context, ApiService service, String method ); } } catch (e) { - debugPrint(e.toString()); messenger?.showSnackBar(const SnackBar(content: Text('Unable to connect to server'))); + + debug += 'FAILED\n $e'; + debugPrint(debug); + return null; } + debug += 'HTTP ${response.statusCode}\n ${response.body}'; + debugPrint(debug); + if (response.statusCode < 200 || response.statusCode >= 300) { try { final json = jsonDecode(response.body); messenger?.showSnackBar(SnackBar(content: Text(json['message'] ?? json['title']))); - debugPrint('API error: ' + json['message']); } catch (e) { - debugPrint(e.toString()); messenger?.showSnackBar(SnackBar(content: Text('Something went wrong (HTTP ${response.statusCode})'))); } return null; diff --git a/Mobile/lib/reviewlist.dart b/Mobile/lib/reviewlist.dart index 4dc2d1b..edbd566 100644 --- a/Mobile/lib/reviewlist.dart +++ b/Mobile/lib/reviewlist.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:mobile/base/sidemenu.dart'; @@ -12,6 +14,37 @@ class ReviewListPage extends StatefulWidget { } class _ReviewListState extends State { + List _users = []; + + models.User? _getReviewUser(models.Review review) { + try { + return _users.firstWhere((user) => user.id == review.userId); + } catch(e) { + return null; + } + } + + @override + void didChangeDependencies() async { + super.didChangeDependencies(); + + final arg = ModalRoute.of(context)!.settings.arguments as models.ReviewList; + final reviews = arg.reviews; + + if (reviews.isEmpty) { + return; + } + + final userIds = reviews.map((review) => review.userId).toSet().toList(); + + final response = await api.request(context, api.ApiService.auth, 'GET', '/api/Users/UsersByIds?userIds=' + userIds.join(','), null); + if (response == null) return; + + setState(() { + _users = (jsonDecode(response) as List).map((user) => models.User.fromJson(user)).toList(); + }); + } + @override Widget build(BuildContext context) { final arg = ModalRoute.of(context)!.settings.arguments as models.ReviewList; @@ -22,54 +55,73 @@ class _ReviewListState extends State { selectedIndex: -1, body: Scaffold( backgroundColor: const Color(0xFFF9F9F9), - body: SingleChildScrollView(child: Container( - decoration: const BoxDecoration(color: Color(0xFFF9F9F9)), - width: MediaQuery.of(context).size.width, - padding: const EdgeInsets.all(20.0), - child: Column(children: - reviews.map((review) => Container( - width: double.infinity, - padding: const EdgeInsets.all(20), - margin: const EdgeInsets.only(bottom: 10), - decoration: const BoxDecoration( - boxShadow: [ - BoxShadow( - color: Color(0x20000000), - offset: Offset(0,1), - blurRadius: 4, + body: reviews.isEmpty + ? const Center(child: Text('No reviews yet. Be the first to review this place')) + : SingleChildScrollView(child: Container( + decoration: const BoxDecoration(color: Color(0xFFF9F9F9)), + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.all(20.0), + child: Column(children: [ + for (final review in reviews) + Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + margin: const EdgeInsets.only(bottom: 10), + decoration: const BoxDecoration( + boxShadow: [ + BoxShadow( + color: Color(0x20000000), + offset: Offset(0,1), + blurRadius: 4, + ), + ], + color: Colors.white, ), - ], - color: Colors.white, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Padding( - padding: EdgeInsets.only(top: 3), - child: Icon(Icons.rate_review, color: Colors.purple, size: 36), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 3), + child: + _getReviewUser(review)?.profilePicture.isNotEmpty == true + ? ClipOval( + child: Image( + image: NetworkImage(_getReviewUser(review)!.profilePicture), + height: 36, + width: 36, + fit: BoxFit.cover, + ), + ) + : const Icon( + Icons.account_circle, + size: 36, + color: Colors.grey, + ) + ), + const SizedBox(width: 20), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(review.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24)), + Text(review.content), + const SizedBox(height: 10), + if (review.image != null) Image.network(review.image!.imageUrl, height: 200), + if (review.image != null) const SizedBox(height: 15), + Row(children: [ + for (var i = 0; i < review.rating; i++) const Icon(Icons.star, color: Colors.yellow), + for (var i = review.rating; i < 5; i++) const Icon(Icons.star_border), + ]), + const SizedBox(height: 10), + Text('Submitted by ' + (_getReviewUser(review)?.username ?? ''), style: const TextStyle(color: Colors.grey, fontSize: 12)), + ], + ), + ), + ], ), - const SizedBox(width: 20), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(review.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24)), - Text(review.content), - const SizedBox(height: 10), - if (review.image != null) Image.network(review.image!.imageUrl, height: 200,), - if (review.image != null) const SizedBox(height: 15), - Row(children: [ - for (var i = 0; i < review.rating; i++) const Icon(Icons.star, color: Colors.yellow), - for (var i = review.rating; i < 5; i++) const Icon(Icons.star_border), - ]), - ], - ), - ), - ], - ), - )).toList(), - ), - )), + ), + ]), + )), floatingActionButton: FloatingActionButton( onPressed: () async { if (!await api.isLoggedIn(context)) { diff --git a/Mobile/pubspec.lock b/Mobile/pubspec.lock index fb0194d..c537257 100644 --- a/Mobile/pubspec.lock +++ b/Mobile/pubspec.lock @@ -194,10 +194,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "4.0.0" flutter_map: dependency: "direct main" description: @@ -412,10 +412,10 @@ packages: dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" lists: dependency: transitive description: diff --git a/Mobile/pubspec.yaml b/Mobile/pubspec.yaml index 9b609b5..3f66b54 100644 --- a/Mobile/pubspec.yaml +++ b/Mobile/pubspec.yaml @@ -53,7 +53,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^3.0.0 + flutter_lints: ^4.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/rust-backend/src/main.rs b/rust-backend/src/main.rs index 004ddbe..ba3e442 100644 --- a/rust-backend/src/main.rs +++ b/rust-backend/src/main.rs @@ -217,9 +217,11 @@ async fn create_review(auth: AuthorizedUser, data: web::Data, input: we place_description: input.place_description.clone(), title: input.title.clone(), content: input.content.clone(), - rating: input.rating.clone(), + rating: input.rating.clone(), image_id: input.image_id, - image: None, + image: input.image_id.and_then(|image_id| { + db.query_row("SELECT * FROM images WHERE id = :id LIMIT 1", &[(":id", &image_id.to_string())], |row| Image::from_row(row)).ok() + }), }), Err(_) => HttpResponse::InternalServerError().finish(), }