Add review images to review list

Co-authored-by: Reimar <mail@reim.ar>
This commit is contained in:
Alexandertp 2024-09-11 12:46:49 +02:00
parent 4d39e40648
commit ebf81906ec
8 changed files with 62 additions and 29 deletions

View File

@ -1,9 +1,18 @@
{ {
"JwtSettings": {
"Issuer": "Flutter-SkanTravels",
"Audience": "Mercantec-Elever",
"Key": "DenHerMåAldrigVæreOffentligKunIDetteDemoProjekt"
},
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Information", "Default": "Information",
"Microsoft.AspNetCore": "Warning" "Microsoft.AspNetCore": "Warning"
} }
}, },
"AllowedHosts": "*" "ConnectionStrings": {
"DefaultConnection": "Data Source=database.sqlite3"
},
"AccessKey": "",
"SecretKey": ""
} }

View File

@ -34,8 +34,9 @@ class Review {
String title; String title;
String content; String content;
int rating; int rating;
Image? image;
Review(this.id, this.userId, this.lat, this.lng, this.place_name, this.place_description, this.title, this.content, this.rating); Review(this.id, this.userId, this.lat, this.lng, this.place_name, this.place_description, this.title, this.content, this.rating, this.image);
factory Review.fromJson(Map<String, dynamic> json) { factory Review.fromJson(Map<String, dynamic> json) {
return Review( return Review(
@ -48,6 +49,7 @@ class Review {
json['title'], json['title'],
json['content'], json['content'],
json['rating'], json['rating'],
json['image'] != null ? Image.fromJson(json['image']) : null,
); );
} }
} }

View File

@ -1,7 +1,7 @@
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 'models.dart'; import 'models.dart' as models;
import 'api.dart' as api; import 'api.dart' as api;
class ReviewListPage extends StatefulWidget { class ReviewListPage extends StatefulWidget {
@ -14,14 +14,14 @@ class ReviewListPage extends StatefulWidget {
class _ReviewListState extends State<ReviewListPage> { class _ReviewListState extends State<ReviewListPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final arg = ModalRoute.of(context)!.settings.arguments as ReviewList; final arg = ModalRoute.of(context)!.settings.arguments as models.ReviewList;
final reviews = arg.reviews; 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: Color(0xFFF9F9F9), backgroundColor: const Color(0xFFF9F9F9),
body: SingleChildScrollView(child: Container( body: SingleChildScrollView(child: Container(
decoration: const BoxDecoration(color: Color(0xFFF9F9F9)), decoration: const BoxDecoration(color: Color(0xFFF9F9F9)),
width: MediaQuery.of(context).size.width, width: MediaQuery.of(context).size.width,
@ -46,7 +46,7 @@ class _ReviewListState extends State<ReviewListPage> {
children: [ children: [
const Padding( const Padding(
padding: EdgeInsets.only(top: 3), padding: EdgeInsets.only(top: 3),
child: Icon(Icons.radio, color: Colors.purple, size: 36), child: Icon(Icons.rate_review, color: Colors.purple, size: 36),
), ),
const SizedBox(width: 20), const SizedBox(width: 20),
Expanded( Expanded(
@ -56,6 +56,8 @@ class _ReviewListState extends State<ReviewListPage> {
Text(review.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24)), Text(review.title, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24)),
Text(review.content), Text(review.content),
const SizedBox(height: 10), 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: [ Row(children: [
for (var i = 0; i < review.rating; i++) const Icon(Icons.star, color: Colors.yellow), 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), for (var i = review.rating; i < 5; i++) const Icon(Icons.star_border),
@ -75,7 +77,7 @@ class _ReviewListState extends State<ReviewListPage> {
return; return;
} }
final review = await Navigator.pushNamed(context, '/create-review', arguments: place) as 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,

View File

@ -473,7 +473,7 @@ packages:
source: hosted source: hosted
version: "1.0.6" version: "1.0.6"
path: path:
dependency: transitive dependency: "direct main"
description: description:
name: path name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"

View File

@ -2262,7 +2262,6 @@ dependencies = [
"actix-utils", "actix-utils",
"actix-web", "actix-web",
"aws-config", "aws-config",
"aws-credential-types",
"aws-sdk-s3", "aws-sdk-s3",
"base64 0.22.1", "base64 0.22.1",
"dotenvy", "dotenvy",

View File

@ -19,4 +19,3 @@ tokio = { version = "1", features = ["full"] }
refinery = { version = "0.8.14", features = ["rusqlite"] } refinery = { version = "0.8.14", features = ["rusqlite"] }
rusqlite = { version = "0.31", features = ["bundled"] } rusqlite = { version = "0.31", features = ["bundled"] }
reqwest = { version = "0.11.16", features = ["blocking", "json"] } reqwest = { version = "0.11.16", features = ["blocking", "json"] }
aws-credential-types = { version = "1.2.1", features = ["hardcoded-credentials"] }

View File

@ -8,10 +8,7 @@ use models::{Favorite, Review, Image};
use serde::Deserialize; use serde::Deserialize;
use actix_web::web::Bytes; use actix_web::web::Bytes;
use aws_sdk_s3::primitives::ByteStream; use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::config::Region;
use rusqlite::types::Null;
use env_logger; use env_logger;
use aws_sdk_s3::config::Credentials;
mod embedded { mod embedded {
use refinery::embed_migrations; use refinery::embed_migrations;
@ -117,23 +114,43 @@ async fn delete_favorite(auth: AuthorizedUser, data:web::Data<AppData>, path: we
} }
#[get("/reviews")] #[get("/reviews")]
async fn reviews(data: web::Data<AppData>) -> impl Responder { async fn get_reviews(data: web::Data<AppData>) -> impl Responder {
let db = data.database.lock().unwrap(); let db = data.database.lock().unwrap();
match get_reviews(db) { match fetch_reviews(db) {
Some(reviews) => HttpResponse::Ok().insert_header(("Content-Type", "application/json; charset=utf-8")).json(reviews), Some(reviews) => HttpResponse::Ok().insert_header(("Content-Type", "application/json; charset=utf-8")).json(reviews),
None => HttpResponse::InternalServerError().finish(), None => HttpResponse::InternalServerError().finish(),
} }
} }
fn get_reviews(db: MutexGuard<'_, rusqlite::Connection>) -> Option<Vec<Review>> { fn fetch_reviews(db: MutexGuard<'_, rusqlite::Connection>) -> Option<Vec<Review>> {
Some( let reviews: Vec<Review> = db.prepare("SELECT * FROM reviews").ok()?
db.prepare("SELECT * FROM reviews").ok()? .query_map([], |row| Review::from_row(row))
.query_map([], |row| Review::from_row(row)) .ok()?
.ok()? .map(|rev| rev.unwrap())
.map(|rev| rev.unwrap()) .collect();
.collect()
) let review_ids = reviews.clone()
.into_iter()
.map(|r| r.id.to_string())
.collect::<Vec<String>>()
.join(",");
let images: Vec<Image> = db.prepare(&format!("SELECT * FROM images WHERE id IN ({})", review_ids)).ok()?
.query_map([], |row| Image::from_row(row))
.ok()?
.map(|img| img.unwrap())
.collect();
Some(reviews.into_iter().map(|r| {
let mut review = r.clone();
if review.image_id.is_some() {
review.image = images.clone().into_iter().find(|img| img.id == review.image_id.unwrap());
}
return review;
}).collect())
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -201,6 +218,8 @@ async fn create_review(auth: AuthorizedUser, data: web::Data<AppData>, input: we
title: input.title.clone(), title: input.title.clone(),
content: input.content.clone(), content: input.content.clone(),
rating: input.rating.clone(), rating: input.rating.clone(),
image_id: input.image_id,
image: None,
}), }),
Err(_) => HttpResponse::InternalServerError().finish(), Err(_) => HttpResponse::InternalServerError().finish(),
} }
@ -241,8 +260,6 @@ async fn create_image(auth: AuthorizedUser, data: web::Data<AppData>, bytes: Byt
let db = data.database.lock().unwrap(); let db = data.database.lock().unwrap();
let config = aws_config::load_from_env().await; let config = aws_config::load_from_env().await;
println!("{:?}", config);
let s3_config = aws_sdk_s3::config::Builder::from(&config) let s3_config = aws_sdk_s3::config::Builder::from(&config)
.force_path_style(true) .force_path_style(true)
.build(); .build();
@ -264,7 +281,7 @@ async fn create_image(auth: AuthorizedUser, data: web::Data<AppData>, bytes: Byt
return HttpResponse::InternalServerError().finish(); return HttpResponse::InternalServerError().finish();
} }
let image_url = format!("{}/{}/{}", bucket_url, bucket_name, query.file_name); let image_url = format!("{}/{}", bucket_url, query.file_name);
match db.execute( match db.execute(
"INSERT INTO images (user_id, image_url) VALUES (:user_id, :image_url)", "INSERT INTO images (user_id, image_url) VALUES (:user_id, :image_url)",
@ -311,12 +328,13 @@ 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))
.service(healthcheck) .service(healthcheck)
.service(authorized) .service(authorized)
.service(favorites) .service(favorites)
.service(create_favorite) .service(create_favorite)
.service(delete_favorite) .service(delete_favorite)
.service(reviews) .service(get_reviews)
.service(create_review) .service(create_review)
.service(delete_review) .service(delete_review)
.service(create_image) .service(create_image)

View File

@ -25,7 +25,7 @@ impl Favorite {
} }
} }
#[derive(Serialize)] #[derive(Serialize, Clone)]
pub struct Review { pub struct Review {
pub id: i64, pub id: i64,
pub user_id: String, pub user_id: String,
@ -36,6 +36,8 @@ pub struct Review {
pub title: String, pub title: String,
pub content: String, pub content: String,
pub rating: i64, pub rating: i64,
pub image_id: Option<i64>,
pub image: Option<Image>,
} }
impl Review { impl Review {
@ -50,11 +52,13 @@ impl Review {
title: row.get("title")?, title: row.get("title")?,
content: row.get("content")?, content: row.get("content")?,
rating: row.get("rating")?, rating: row.get("rating")?,
image_id: row.get("image_id").ok(),
image: None,
}) })
} }
} }
#[derive(Serialize)] #[derive(Serialize, Clone)]
pub struct Image { pub struct Image {
pub id: i64, pub id: i64,
pub user_id: String, pub user_id: String,