Add review images to review list
Co-authored-by: Reimar <mail@reim.ar>
This commit is contained in:
parent
4d39e40648
commit
ebf81906ec
@ -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": ""
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
|
1
rust-backend/Cargo.lock
generated
1
rust-backend/Cargo.lock
generated
@ -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",
|
||||||
|
@ -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"] }
|
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user