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": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"ConnectionStrings": {
"DefaultConnection": "Data Source=database.sqlite3"
},
"AccessKey": "",
"SecretKey": ""
}

View File

@ -34,8 +34,9 @@ class Review {
String title;
String content;
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) {
return Review(
@ -48,6 +49,7 @@ class Review {
json['title'],
json['content'],
json['rating'],
json['image'] != null ? Image.fromJson(json['image']) : null,
);
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:mobile/base/sidemenu.dart';
import 'models.dart';
import 'models.dart' as models;
import 'api.dart' as api;
class ReviewListPage extends StatefulWidget {
@ -14,14 +14,14 @@ class ReviewListPage extends StatefulWidget {
class _ReviewListState extends State<ReviewListPage> {
@override
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 place = arg.place;
return SideMenu(
selectedIndex: -1,
body: Scaffold(
backgroundColor: Color(0xFFF9F9F9),
backgroundColor: const Color(0xFFF9F9F9),
body: SingleChildScrollView(child: Container(
decoration: const BoxDecoration(color: Color(0xFFF9F9F9)),
width: MediaQuery.of(context).size.width,
@ -46,7 +46,7 @@ class _ReviewListState extends State<ReviewListPage> {
children: [
const Padding(
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),
Expanded(
@ -56,6 +56,8 @@ class _ReviewListState extends State<ReviewListPage> {
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),
@ -75,7 +77,7 @@ class _ReviewListState extends State<ReviewListPage> {
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);
},
backgroundColor: Colors.blue,

View File

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

View File

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

View File

@ -19,4 +19,3 @@ tokio = { version = "1", features = ["full"] }
refinery = { version = "0.8.14", features = ["rusqlite"] }
rusqlite = { version = "0.31", features = ["bundled"] }
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 actix_web::web::Bytes;
use aws_sdk_s3::primitives::ByteStream;
use aws_sdk_s3::config::Region;
use rusqlite::types::Null;
use env_logger;
use aws_sdk_s3::config::Credentials;
mod embedded {
use refinery::embed_migrations;
@ -117,23 +114,43 @@ async fn delete_favorite(auth: AuthorizedUser, data:web::Data<AppData>, path: we
}
#[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();
match get_reviews(db) {
match fetch_reviews(db) {
Some(reviews) => HttpResponse::Ok().insert_header(("Content-Type", "application/json; charset=utf-8")).json(reviews),
None => HttpResponse::InternalServerError().finish(),
}
}
fn get_reviews(db: MutexGuard<'_, rusqlite::Connection>) -> Option<Vec<Review>> {
Some(
db.prepare("SELECT * FROM reviews").ok()?
.query_map([], |row| Review::from_row(row))
.ok()?
.map(|rev| rev.unwrap())
.collect()
)
fn fetch_reviews(db: MutexGuard<'_, rusqlite::Connection>) -> Option<Vec<Review>> {
let reviews: Vec<Review> = db.prepare("SELECT * FROM reviews").ok()?
.query_map([], |row| Review::from_row(row))
.ok()?
.map(|rev| rev.unwrap())
.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)]
@ -201,6 +218,8 @@ async fn create_review(auth: AuthorizedUser, data: web::Data<AppData>, input: we
title: input.title.clone(),
content: input.content.clone(),
rating: input.rating.clone(),
image_id: input.image_id,
image: None,
}),
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 config = aws_config::load_from_env().await;
println!("{:?}", config);
let s3_config = aws_sdk_s3::config::Builder::from(&config)
.force_path_style(true)
.build();
@ -264,7 +281,7 @@ async fn create_image(auth: AuthorizedUser, data: web::Data<AppData>, bytes: Byt
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(
"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 {
database: conn.clone(),
}))
.app_data(web::PayloadConfig::new(8_388_608))
.service(healthcheck)
.service(authorized)
.service(favorites)
.service(create_favorite)
.service(delete_favorite)
.service(reviews)
.service(get_reviews)
.service(create_review)
.service(delete_review)
.service(create_image)

View File

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