Compare commits

...

22 Commits

Author SHA1 Message Date
8d632fb84a Update Docs/SCRUM Logbog.md 2024-09-17 10:56:22 +01:00
0957f219fb
Increase file size limit to 30mb 2024-09-17 10:26:19 +02:00
c58daeb586
Add link to figma 2024-09-17 10:26:10 +02:00
25ebe2c766 Update Docs/case.md 2024-09-17 09:22:18 +01:00
3610454680 Update Docs/SCRUM Logbog.md 2024-09-17 09:04:01 +01:00
2a5c283c9a
Add .env to setup instructions for flutter 2024-09-17 09:28:47 +02:00
868e8d8b7c
Re-add .env.example in rust 2024-09-17 09:27:57 +02:00
bdb5647e88
Merge remote-tracking branch 'origin/openai-implementation' 2024-09-17 09:26:00 +02:00
d89c833de2
Add logs for last week 2024-09-17 08:58:34 +02:00
275416ea3c
Fix retrieving location not working on android 2024-09-16 20:38:42 +02:00
32793f2129
Add app name and icon for android app 2024-09-16 13:52:49 +02:00
3922906a33
Fix errors in prod backend and android app 2024-09-16 13:52:49 +02:00
66640261c3 Update Docs/SCRUM Logbog.md 2024-09-16 11:32:51 +01:00
30461189bb
Add setup guide 2024-09-16 11:25:24 +02:00
e8ed845f10
Implement deleting reviews 2024-09-13 12:38:24 +02:00
f44cd9cc9f
Merge branch 'review-user-images' 2024-09-13 12:13:11 +02:00
68c8064083
Show profile picture on reviews 2024-09-13 12:12:58 +02:00
0ab5fee8b2
Show image immediately after submitting review 2024-09-13 12:06:45 +02:00
eca2b6324b
Fix errors on review page, show text when no reviews 2024-09-13 12:02:22 +02:00
90fccde9e6
Add better debugging for http requests 2024-09-13 12:02:05 +02:00
c389381b1e
Fix userIds request not working, show username on reviews 2024-09-13 08:40:32 +02:00
4480080496 Implement backend for displaying user info
Frontend display currently not working

Co-authored-by: Reimar <mail@reim.ar>
2024-09-11 15:34:03 +02:00
27 changed files with 324 additions and 111 deletions

1
.gitignore vendored
View File

@ -3,4 +3,3 @@ API/bin
API/obj API/obj
.idea .idea
/rust-backend/.env.example

View File

@ -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<ActionResult<List<UserDTO>>> Handle(List<string> ids)
{
List<User> 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 }));
}
}
}

View File

@ -22,36 +22,35 @@ namespace API.Controllers
{ {
private readonly QueryAllUsers _queryAllUsers; private readonly QueryAllUsers _queryAllUsers;
private readonly QueryUserById _queryUserById; private readonly QueryUserById _queryUserById;
private readonly QueryUsersByIds _queryUsersByIds;
private readonly CreateUser _createUser; private readonly CreateUser _createUser;
private readonly UpdateUser _updateUser; private readonly UpdateUser _updateUser;
private readonly DeleteUser _deleteUser; private readonly DeleteUser _deleteUser;
private readonly LoginUser _loginUser; private readonly LoginUser _loginUser;
private readonly TokenHelper _tokenHelper; private readonly TokenHelper _tokenHelper;
private readonly IUserRepository _repository; private readonly IUserRepository _repository;
public UsersController( public UsersController(
QueryAllUsers queryAllUsers, QueryAllUsers queryAllUsers,
QueryUserById queryUserById, QueryUserById queryUserById,
QueryUsersByIds queryUsersByIds,
CreateUser createUser, CreateUser createUser,
UpdateUser updateUser, UpdateUser updateUser,
DeleteUser deleteUser, DeleteUser deleteUser,
LoginUser loginUser, LoginUser loginUser,
TokenHelper tokenHelper, TokenHelper tokenHelper,
IUserRepository repository IUserRepository repository
) )
{ {
_queryAllUsers = queryAllUsers; _queryAllUsers = queryAllUsers;
_queryUserById = queryUserById; _queryUserById = queryUserById;
_queryUsersByIds = queryUsersByIds;
_createUser = createUser; _createUser = createUser;
_updateUser = updateUser; _updateUser = updateUser;
_deleteUser = deleteUser; _deleteUser = deleteUser;
_loginUser = loginUser; _loginUser = loginUser;
_tokenHelper = tokenHelper; _tokenHelper = tokenHelper;
_repository = repository; _repository = repository;
} }
[HttpPost("login")] [HttpPost("login")]
@ -73,9 +72,16 @@ namespace API.Controllers
return await _queryUserById.Handle(id); return await _queryUserById.Handle(id);
} }
[HttpGet("UsersByIds")]
public async Task<ActionResult<List<UserDTO>>> GetUsersByIds(string userIds)
{
List<string> ids = userIds.Split(",").ToList();
return await _queryUsersByIds.Handle(ids);
}
[Authorize] [Authorize]
[HttpPut] [HttpPut]
public async Task<IActionResult> PutUser([FromForm ]UpdateUserDTO UpdateUserDTO) public async Task<IActionResult> PutUser([FromForm] UpdateUserDTO UpdateUserDTO)
{ {
return await _updateUser.Handle(UpdateUserDTO); return await _updateUser.Handle(UpdateUserDTO);
} }

View File

@ -8,6 +8,7 @@ namespace API.Persistence.Repositories
Task<bool> DeleteUserAsync(string id); Task<bool> DeleteUserAsync(string id);
Task<List<User>> QueryAllUsersAsync(); Task<List<User>> QueryAllUsersAsync();
Task<User> QueryUserByIdAsync(string id); Task<User> QueryUserByIdAsync(string id);
Task<List<User>> QueryUsersByIdsAsync(List<string> ids);
Task<User> QueryUserByEmailAsync(string email); Task<User> QueryUserByEmailAsync(string email);
Task<bool> UpdateUserAsync(User user); Task<bool> UpdateUserAsync(User user);
Task<User> QueryUserByRefreshTokenAsync(string refreshToken); Task<User> QueryUserByRefreshTokenAsync(string refreshToken);

View File

@ -1,5 +1,6 @@
using API.Models; using API.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages; using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages;
namespace API.Persistence.Repositories namespace API.Persistence.Repositories
@ -25,6 +26,18 @@ namespace API.Persistence.Repositories
} }
} }
public async Task<List<User>> QueryUsersByIdsAsync(List<string> ids)
{
try
{
return _context.Users.Where(user => ids.Contains(user.Id)).ToList();
}
catch (Exception)
{
return [];
}
}
public async Task<string> CreateUserAsync(User user) public async Task<string> CreateUserAsync(User user)
{ {
try try

View File

@ -43,7 +43,7 @@ namespace API
builder.Services.AddScoped<DeleteUser>(); builder.Services.AddScoped<DeleteUser>();
builder.Services.AddScoped<LoginUser>(); builder.Services.AddScoped<LoginUser>();
builder.Services.AddScoped<IUserRepository, UserRepository>(); builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<QueryUsersByIds>();
IConfiguration Configuration = builder.Configuration; IConfiguration Configuration = builder.Configuration;

View File

@ -8,6 +8,7 @@ Environment=PATH=$PATH:/home/reimar/.dotnet
Environment=DEFAULT_CONNECTION="Data Source=/home/reimar/skantravels/database.sqlite3" Environment=DEFAULT_CONNECTION="Data Source=/home/reimar/skantravels/database.sqlite3"
ExecStartPre=/home/reimar/skantravels/efbundle ExecStartPre=/home/reimar/skantravels/efbundle
ExecStart=/home/reimar/skantravels/API --urls=http://0.0.0.0:5001 ExecStart=/home/reimar/skantravels/API --urls=http://0.0.0.0:5001
WorkingDirectory=/home/reimar/skantravels
Type=simple Type=simple
[Install] [Install]

View File

@ -151,7 +151,7 @@ KC - Kundechef
## Torsdag ## Torsdag
**SM(Alexander):** **SM(Alexander):** Samarbejdede med Reimar omkring Refresh-tokens, mest med ide-fasen
**PO(Reimar):** Begyndt på implementering af refresh tokens, fikset en fejl med en openssl dependency til vores release-flow **PO(Reimar):** Begyndt på implementering af refresh tokens, fikset en fejl med en openssl dependency til vores release-flow
@ -159,7 +159,7 @@ KC - Kundechef
## Fredag ## Fredag
**SM(Alexander):** **SM(Alexander):** Holdt kundemøder
**PO(Reimar):** Holdt kundemøder **PO(Reimar):** Holdt kundemøder
@ -177,7 +177,7 @@ KC - Kundechef
## Tirsdag ## Tirsdag
**PO(Alexander):** **PO(Alexander):** Pair-programmede med Reimar om nedenstående features
**KC(Reimar):** Implementeret reviews i rust backenden, fået vist dem som location pins på kortet **KC(Reimar):** Implementeret reviews i rust backenden, fået vist dem som location pins på kortet
@ -185,7 +185,7 @@ KC - Kundechef
## Onsdag ## Onsdag
**PO(Alexander):** **PO(Alexander):** Pair-programmede med Reimar om nedenstående features
**KC(Reimar):** Implementeret review-liste når man klikker ind på én **KC(Reimar):** Implementeret review-liste når man klikker ind på én
@ -193,7 +193,7 @@ KC - Kundechef
## Torsdag ## Torsdag
**PO(Alexander):** **PO(Alexander):** Pair-programmede med Reimar om nedenstående features
**KC(Reimar):** Lavet create review-side, tilføjet stjerner til review-liste **KC(Reimar):** Lavet create review-side, tilføjet stjerner til review-liste
@ -201,7 +201,7 @@ KC - Kundechef
## Fredag ## Fredag
**PO(Alexander):** **PO(Alexander):** Hjalp med kundemøder
**KC(Reimar):** Holdt kundemøder **KC(Reimar):** Holdt kundemøder
@ -211,49 +211,59 @@ KC - Kundechef
## Mandag ## Mandag
**KC(Alexander):** **KC(Alexander):** Aftalte udviklermøde
**SM(Reimar):** **SM(Reimar):** Påbegyndt implementation af AWS image upload i Rust
**PO(Philip):** **PO(Philip):** Fik billede upload til at virke efter reimar sagde at jeg skulle sende fra flutter som en form i stedet for JSON
## Tirsdag ## Tirsdag
**KC(Alexander):** **KC(Alexander):**
**SM(Reimar):** **SM(Reimar):** Færdiggjort image uploads i Rust
**PO(Philip):** **PO(Philip):** intet med kundegruppen. Fik billede til at vise icon hvis der ikke var billede. og det nye billede blev vist efter man havde savet
## Onsdag ## Onsdag
**KC(Alexander):** **KC(Alexander):** Afholdte udviklermøde
**SM(Reimar):** **SM(Reimar):** Tilføjet billeder til reviews, lavet backend til at vise bruger-information på review-liste
**PO(Philip):** **PO(Philip):** Kiggede på openai. planlage møde med kundegruppe
## Torsdag ## Torsdag
**KC(Alexander):** **KC(Alexander):**
**SM(Reimar):** **SM(Reimar):** Holdt møde, fikset min cykel
**PO(Philip):** **PO(Philip):** fik overblik over hvad vi hver især skulle vise til kundegruppe mødet. openai blev ikke færdig
## Fredag ## Fredag
**KC(Alexander):** **KC(Alexander):**
**SM(Reimar):** **SM(Reimar):** Lavet frontend til at vise bruger-info på review-liste, implementeret sletning af reviews, fikset nogle fejl ved image upload
**PO(Philip):** **PO(Philip):** Møde med kundegruppen hvor vi viste alt funktionallitet, dog ikke at det virkede fuldendt
## Fredag # Uge 38
**KC(Alexander):** ## Mandag
**SM(Reimar):** **SM(Alexander):**
**PO(Philip):** **PO(Reimar):**
**KC(Philip):** openai blev færdiglavet med guidebook
## Tirsdag
**SM(Alexander):**
**PO(Reimar):**
**KC(Philip):** for overblik over hvad vi skal sige til eksamen

View File

@ -1,3 +1,3 @@
# Case beskrivelse # Case beskrivelse
Som kunde skal I beskrive det product som I ønsker! For eksempler se her - [Notion](https://mercantec.notion.site/Casebeskrivelse-og-Kravspec-60eb806216074896ae1b3c7f14d9b2b6?pvs=4) PDF[Case](https://edumercantec.sharepoint.com/:b:/s/24Q3H4-AppprogrammeringServerogmetodik/EbZ4bfQ_4xhJlUp6dVJMmbEBq7saaqfJJ8RuHLuPnVe6hw?e=yQoxXO)

22
Docs/setup.md Normal file
View File

@ -0,0 +1,22 @@
## Setting up
### C# backend
In the `API` folder, copy `appsettings.example.json` to `appsettings.json` and fill out the values. `AccessKey` and `SecretKey` are for the Cloudflare R2 service.
Run `dotnet ef database update` and then `dotnet run`.
### Rust backend
In the `rust-backend` folder, copy `.env.example` to `.env` and fill out the values. Make sure the JWT secret is the same on both backends.
Rust can be installed from <https://rustup.rs>. After installation, run `rustup default stable` in the terminal.
To start the backend, run `cargo run`.
### Flutter
In the `Mobile` folder, copy `environment.example.json` to `environment.json` and fill out the values. Also do this with `.env.example`, copying it into `.env`.
Run `flutter run --dart-define-from-file environment.json`.

View File

@ -3,7 +3,7 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
android:label="mobile" android:label="SkanTravels"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 18 KiB

4
Mobile/build-apk.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
java -version
flutter build apk --split-per-abi --no-shrink --dart-define-from-file environment.prod.json

View File

@ -0,0 +1,4 @@
{
"AUTH_SERVICE_HOST": "https://skantravels.reim.ar",
"APP_SERVICE_HOST": "https://skantravels.reim.ar"
}

View File

@ -20,7 +20,11 @@ import 'services/api.dart' as api;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: ".env"); await dotenv.load(fileName: ".env");
// Refresh JWT on startup
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
if (prefs.getString("token") != null && prefs.getString("refresh-token") != null) { if (prefs.getString("token") != null && prefs.getString("refresh-token") != null) {
final token = await api.request(null, api.ApiService.auth, "POST", "/RefreshToken", {'refreshToken': prefs.getString("refresh-token")}); final token = await api.request(null, api.ApiService.auth, "POST", "/RefreshToken", {'refreshToken': prefs.getString("refresh-token")});
@ -250,38 +254,47 @@ class _MyHomePageState extends State<MyHomePage> {
}); });
} }
Future<void> _onSearch() async { Future<void> _onSearch() async {
final http.Response response = await http.get( final http.Response response = await http.get(
Uri.parse('https://nominatim.openstreetmap.org/search.php?q=${searchBarInput.text}&format=jsonv2'), Uri.parse('https://nominatim.openstreetmap.org/search.php?q=${searchBarInput.text}&format=jsonv2'),
headers: {'User-Agent': 'SkanTravels/1.0'} headers: {'User-Agent': 'SkanTravels/1.0'}
); );
final dynamic location = jsonDecode(response.body); final dynamic location = jsonDecode(response.body);
// Move the map to the center of the first search result // Move the map to the center of the first search result
_mapController.move( _mapController.move(
LatLng(double.parse(location[0]['lat']), double.parse(location[0]['lon'])), LatLng(double.parse(location[0]['lat']), double.parse(location[0]['lon'])),
8 8
); );
// Extract the bounding box and convert to LatLng // Extract the bounding box and convert to LatLng
final List<dynamic> boundingBox = location[0]['boundingbox']; final List<dynamic> boundingBox = location[0]['boundingbox'];
_getOpenStreetMapData(LatLng(double.parse(boundingBox[0]), double.parse(boundingBox[2])), LatLng(double.parse(boundingBox[1]), double.parse(boundingBox[3]))); _getOpenStreetMapData(LatLng(double.parse(boundingBox[0]), double.parse(boundingBox[2])), LatLng(double.parse(boundingBox[1]), double.parse(boundingBox[3])));
} }
Future<void> _getCurrentLocation() async { Future<void> _getCurrentLocation() async {
LocationPermission permission = await Geolocator.checkPermission(); LocationPermission? permission;
if(permission != LocationPermission.always || permission != LocationPermission.whileInUse){ try {
permission = await Geolocator.requestPermission();
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error retrieving location: $e')));
return;
} }
else{
await Geolocator.requestPermission(); if (permission != LocationPermission.always && permission != LocationPermission.whileInUse) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Location permission denied')));
return;
} }
Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high); Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
_mapController.move(LatLng(position.latitude, position.longitude), 10); _mapController.move(LatLng(position.latitude, position.longitude), 10);
setState(() { setState(() {
_userPosition = LatLng(position.latitude, position.longitude); _userPosition = LatLng(position.latitude, position.longitude);
}); });
LatLngBounds bounds = _mapController.camera.visibleBounds; LatLngBounds bounds = _mapController.camera.visibleBounds;
_getOpenStreetMapData(LatLng(bounds.southWest.latitude, bounds.southWest.longitude),LatLng(bounds.northEast.latitude, bounds.northEast.longitude)); _getOpenStreetMapData(LatLng(bounds.southWest.latitude, bounds.southWest.longitude),LatLng(bounds.northEast.latitude, bounds.northEast.longitude));

View File

@ -1,6 +1,9 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mobile/base/sidemenu.dart'; import 'package:mobile/base/sidemenu.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'models.dart' as models; import 'models.dart' as models;
import 'services/api.dart' as api; import 'services/api.dart' as api;
@ -12,64 +15,147 @@ class ReviewListPage extends StatefulWidget {
} }
class _ReviewListState extends State<ReviewListPage> { class _ReviewListState extends State<ReviewListPage> {
List<models.User> _users = [];
List<models.Review> _reviews = [];
String? _currentUserId;
models.User? _getReviewUser(models.Review review) {
try {
return _users.firstWhere((user) => user.id == review.userId);
} catch(e) {
return null;
}
}
void _confirmDeleteReview(models.Review review) {
showDialog(context: context, builder: (BuildContext context) =>
AlertDialog(
title: const Text('Delete review'),
content: const Text('Are you sure you want to delete this review?'),
actions: [
TextButton(child: const Text('Cancel'), onPressed: () => Navigator.pop(context)),
TextButton(child: const Text('Delete', style: TextStyle(color: Colors.red)), onPressed: () => _deleteReview(review)),
]
)
);
}
void _deleteReview(models.Review review) async {
Navigator.pop(context);
final response = await api.request(context, api.ApiService.app, 'DELETE', '/reviews/${review.id}', null);
if (response == null) return;
setState(() {
_reviews = _reviews.where((r) => r.id != review.id).toList();
});
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Review deleted successfully')));
}
@override
void didChangeDependencies() async {
super.didChangeDependencies();
final prefs = await SharedPreferences.getInstance();
_currentUserId = prefs.getString('id');
final arg = ModalRoute.of(context)!.settings.arguments as models.ReviewList;
_reviews = arg.reviews;
if (_reviews.isEmpty || !mounted) {
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<dynamic>).map((user) => models.User.fromJson(user)).toList();
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final arg = ModalRoute.of(context)!.settings.arguments as models.ReviewList; final arg = ModalRoute.of(context)!.settings.arguments as models.ReviewList;
final reviews = arg.reviews;
final place = arg.place; final place = arg.place;
return SideMenu( return SideMenu(
selectedIndex: -1, selectedIndex: -1,
body: Scaffold( body: Scaffold(
backgroundColor: const Color(0xFFF9F9F9), backgroundColor: const Color(0xFFF9F9F9),
body: SingleChildScrollView(child: Container( body: _reviews.isEmpty
decoration: const BoxDecoration(color: Color(0xFFF9F9F9)), ? const Center(child: Text('No reviews yet. Be the first to review this place'))
width: MediaQuery.of(context).size.width, : SingleChildScrollView(child: Container(
padding: const EdgeInsets.all(20.0), decoration: const BoxDecoration(color: Color(0xFFF9F9F9)),
child: Column(children: width: MediaQuery.of(context).size.width,
reviews.map((review) => Container( padding: const EdgeInsets.all(20.0),
width: double.infinity, child: Column(children: [
padding: const EdgeInsets.all(20), for (final review in _reviews)
margin: const EdgeInsets.only(bottom: 10), Container(
decoration: const BoxDecoration( width: double.infinity,
boxShadow: [ padding: const EdgeInsets.all(20),
BoxShadow( margin: const EdgeInsets.only(bottom: 10),
color: Color(0x20000000), decoration: const BoxDecoration(
offset: Offset(0,1), boxShadow: [
blurRadius: 4, BoxShadow(
color: Color(0x20000000),
offset: Offset(0,1),
blurRadius: 4,
),
],
color: Colors.white,
), ),
], child: Row(
color: Colors.white, crossAxisAlignment: CrossAxisAlignment.start,
), children: [
child: Row( Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.only(top: 3),
children: [ child:
const Padding( _getReviewUser(review)?.profilePicture.isNotEmpty == true
padding: EdgeInsets.only(top: 3), ? ClipOval(
child: Icon(Icons.rate_review, color: Colors.purple, size: 36), 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: [
Row(children: [
Expanded(child: Text(review.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24))),
if (review.userId == _currentUserId) IconButton(onPressed: () => _confirmDeleteReview(review), icon: const Icon(Icons.delete, color: Colors.grey)),
]),
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( floatingActionButton: FloatingActionButton(
onPressed: () async { onPressed: () async {
if (!await api.isLoggedIn(context)) { if (!await api.isLoggedIn(context)) {
@ -78,7 +164,7 @@ class _ReviewListState extends State<ReviewListPage> {
} }
final review = await Navigator.pushNamed(context, '/create-review', arguments: place) as models.Review?; final review = await Navigator.pushNamed(context, '/create-review', arguments: place) as models.Review?;
if (review != null) reviews.add(review); if (review != null) _reviews.add(review);
}, },
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
focusColor: Colors.blueGrey, focusColor: Colors.blueGrey,

View File

@ -13,6 +13,8 @@ enum ApiService {
} }
Future<String?> request(BuildContext? context, ApiService service, String method, String path, dynamic body) async { Future<String?> 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 messenger = context != null ? ScaffoldMessenger.of(context) : null;
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
@ -47,18 +49,22 @@ Future<String?> request(BuildContext? context, ApiService service, String method
); );
} }
} catch (e) { } catch (e) {
debugPrint(e.toString());
messenger?.showSnackBar(const SnackBar(content: Text('Unable to connect to server'))); messenger?.showSnackBar(const SnackBar(content: Text('Unable to connect to server')));
debug += 'FAILED\n $e';
debugPrint(debug);
return null; return null;
} }
debug += 'HTTP ${response.statusCode}\n ${response.body}';
debugPrint(debug);
if (response.statusCode < 200 || response.statusCode >= 300) { if (response.statusCode < 200 || response.statusCode >= 300) {
try { try {
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
messenger?.showSnackBar(SnackBar(content: Text(json['message'] ?? json['title']))); messenger?.showSnackBar(SnackBar(content: Text(json['message'] ?? json['title'])));
debugPrint('API error: ' + json['message']);
} catch (e) { } catch (e) {
debugPrint(e.toString());
messenger?.showSnackBar(SnackBar(content: Text('Something went wrong (HTTP ${response.statusCode})'))); messenger?.showSnackBar(SnackBar(content: Text('Something went wrong (HTTP ${response.statusCode})')));
} }
return null; return null;

View File

@ -210,10 +210,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" version: "4.0.0"
flutter_map: flutter_map:
dependency: "direct main" dependency: "direct main"
description: description:
@ -428,10 +428,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "4.0.0"
lists: lists:
dependency: transitive dependency: transitive
description: description:

View File

@ -55,7 +55,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your # activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint # package. See that file for information about deactivating specific lint
# rules and activating additional ones. # 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 # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View File

@ -6,3 +6,8 @@ Vi vil udvikle en TuristApp for SkanTravles, som gør brug af telefonens sensore
[Kravspecifikation](Docs/kravspec.md) [Kravspecifikation](Docs/kravspec.md)
[Logbog](Docs/SCRUM%20Logbog.md) [Logbog](Docs/SCRUM%20Logbog.md)
[Setup guide](Docs/setup.md)
[Figma design](https://www.figma.com/design/r5EkReHw6NVC3gd9DeIc0P/Gruppe-6---SkanTravels?node-id=0-1&t=FxBQswcVrzRnhR2T-1)

View File

@ -0,0 +1,8 @@
JWT_SECRET=DenHerMåAldrigVæreOffentligKunIDetteDemoProjekt
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_ENDPOINT_URL=
AWS_REGION=
R2_BUCKET_NAME=
R2_BUCKET_URL=

View File

@ -6,6 +6,7 @@ After=network.target
Environment=RUST_BACKEND_PORT=5002 Environment=RUST_BACKEND_PORT=5002
Environment=RUST_BACKEND_DB=/home/reimar/skantravels/database-rust.sqlite3 Environment=RUST_BACKEND_DB=/home/reimar/skantravels/database-rust.sqlite3
ExecStart=/home/reimar/skantravels/skantravels ExecStart=/home/reimar/skantravels/skantravels
WorkingDirectory=/home/reimar/skantravels
Type=simple Type=simple
[Install] [Install]

View File

@ -219,7 +219,9 @@ async fn create_review(auth: AuthorizedUser, data: web::Data<AppData>, input: we
content: input.content.clone(), content: input.content.clone(),
rating: input.rating.clone(), rating: input.rating.clone(),
image_id: input.image_id, 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(), Err(_) => HttpResponse::InternalServerError().finish(),
} }
@ -328,7 +330,7 @@ async fn main() -> std::io::Result<()> {
.app_data(web::Data::new(AppData { .app_data(web::Data::new(AppData {
database: conn.clone(), database: conn.clone(),
})) }))
.app_data(web::PayloadConfig::new(8_388_608)) .app_data(web::PayloadConfig::new(30 * 1024 * 1024))
.service(healthcheck) .service(healthcheck)
.service(authorized) .service(authorized)
.service(favorites) .service(favorites)