diff --git a/Cargo.lock b/Cargo.lock index 6ae120a..87faa22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -795,6 +795,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1096,6 +1106,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "once_cell", "percent-encoding", "pin-project-lite", @@ -1575,6 +1586,15 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.11" diff --git a/Cargo.toml b/Cargo.toml index 30e3dcf..355b1a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ js-sys = "0.3" web-sys = "0.3.61" # other libs -reqwest = { version = "0.11.11", features = ["rustls-tls", "json", "blocking"], default-features = false } +reqwest = { version = "0.11.11", features = ["rustls-tls", "json", "blocking", "multipart"], default-features = false } uuid = { version = "1.3.0", features = ["v4", "fast-rng", "macro-diagnostics", "serde", "js"] } log = "0.4" serde = { version = "1.0", features = ["derive"] } diff --git a/src/api/backend/mod.rs b/src/api/backend/mod.rs index ea93164..bd73987 100644 --- a/src/api/backend/mod.rs +++ b/src/api/backend/mod.rs @@ -5,12 +5,13 @@ use jl_types::{ dto::{ filters::Filter, listing::Listing, payloads::contact::ContactPayload, project_card::ProjectCardDto, + item::Item, }, }; use reqwest::Method; use uuid::Uuid; -use super::base::perform_request_without_client; +use super::base::{perform_request_without_client, perform_multipart_request_without_client}; const BASE_URL: &str = "http://localhost:8095/"; @@ -148,7 +149,16 @@ pub async fn create_new_contact_request(contact: ContactPayload) -> Result<(), e ) .await } -/* -pub async fn upload_image() -> Result { - perform_request_without_client(BASE_URL.into(), Method::POST, format!("images/"), Some(), 200, Vec::new(), None).await -}*/ + +pub async fn upload_image(item: Item, body: Vec) -> Result { + perform_multipart_request_without_client( + BASE_URL.into(), + Method::POST, + format!("admin/images/{item}"), + body, + 200, + Vec::new(), + None, + ) + .await +} diff --git a/src/api/base.rs b/src/api/base.rs index 4615576..ab35d1f 100644 --- a/src/api/base.rs +++ b/src/api/base.rs @@ -60,3 +60,59 @@ pub async fn perform_request_without_client( } } } + +pub async fn perform_multipart_request_without_client( + base_url: String, + method: reqwest::Method, + path: String, + body: Vec, + expected_status_code: u16, + headers: Vec<(String, String)>, + params: Option>, +) -> Result { + let client = Client::new(); + let mut req_incomplete = + client.request(method, format!("{url}{path}", url = base_url, path = path)); + + for header in headers { + req_incomplete = req_incomplete.header(&header.0, &header.1); + } + + if let Some(parameters) = params { + req_incomplete = req_incomplete.query(¶meters) + } + + let part = reqwest::multipart::Part::bytes(body).file_name("image"); + let form = reqwest::multipart::Form::new(); + let form = form.part("image", part); + + match req_incomplete.multipart(form).send().await { + // Error handling here + Ok(res) => { + // Request sent correctly + match res.status().as_u16() == expected_status_code { + true => { + match res.json::().await { + Ok(resp_dto) => Ok(resp_dto), // Return correctly deserialized obj + Err(err) => Err(Error::Serde(MessageResource::from(err))), + } + } + false => { + //If status code is any other than expected + Err(Error::UnexpectedStatusCode( + expected_status_code, + res.status().as_u16(), + match res.json::>().await { + Ok(messages) => messages, + Err(e) => vec![MessageResource::from(e)], + }, + )) + } + } + } + Err(e) => { + // Request couldn't be sent + Err(Error::Network(MessageResource::from(e))) + } + } +} diff --git a/src/components/media_picker.rs b/src/components/media_picker.rs index cb9f35e..01a5f39 100644 --- a/src/components/media_picker.rs +++ b/src/components/media_picker.rs @@ -1,7 +1,9 @@ use jl_types::domain::media::{Media, MediaList}; +use js_sys::Uint8Array; +use wasm_bindgen_futures::JsFuture; use yew::prelude::*; -use crate::components::textfield::get_value_from_input_event; +use crate::{components::textfield::{get_value_from_input_event, get_files_from_input_event}, api}; #[function_component(MediaPicker)] pub fn media_picker(props: &MediaPickerProps) -> Html { @@ -9,7 +11,7 @@ pub fn media_picker(props: &MediaPickerProps) -> Html { <>
{"Media del proyecto"}
- +
} @@ -21,6 +23,7 @@ pub struct MediaPickerProps { pub onchange: Option>, #[prop_or_default] pub required: bool, + pub item: jl_types::dto::item::Item, } #[function_component(MediaListRendered)] fn render_media_list(props: &MediaListProps) -> Html { @@ -90,15 +93,42 @@ fn render_media_list(props: &MediaListProps) -> Html { let onphoto_upload = { let media_handle = props.medialist.clone(); let onchange_cb = props.onchange.clone(); - Callback::from(move |_: InputEvent| { + let item = props.item.clone(); + Callback::from(move |e: InputEvent| { + match onchange_cb.clone() { Some(cb) => cb.emit(String::new()), None => {} }; - //TODO: Upload picture, then push url into medialist - let mut media = (*media_handle).clone(); - media.media_list.push(Media::Photo("".into())); - media_handle.set(media); + let files_opt = get_files_from_input_event(e); + match files_opt { + Some(filelist) => { + match filelist.get(0) { + Some(file) => { + let media_handle = media_handle.clone(); + let item = item.clone(); + wasm_bindgen_futures::spawn_local(async move { + let array_buf = JsFuture::from(file.array_buffer()).await; + let array = Uint8Array::new(&array_buf.unwrap()); + let bytes: Vec = array.to_vec(); + // Upload to backend and retrieve url + match api::backend::upload_image(item, bytes).await { + Ok(url) => { + let mut media = (*media_handle).clone(); + media.media_list.push(Media::Photo(url)); + media_handle.set(media); + }, + Err(error) => log::error!("Error uploading image to backend: {error}"), + }; + }); + }, + None => {log::error!("No files in first element of filelist...")} + }; + }, + None => { + log::error!("Something weird happened. No files after selecting files") + } + } }) }; @@ -126,4 +156,5 @@ fn render_media_list(props: &MediaListProps) -> Html { pub struct MediaListProps { pub medialist: UseStateHandle, pub onchange: Option>, + pub item: jl_types::dto::item::Item, } diff --git a/src/components/textfield.rs b/src/components/textfield.rs index 2490d84..d39269e 100644 --- a/src/components/textfield.rs +++ b/src/components/textfield.rs @@ -79,3 +79,9 @@ pub fn get_value_from_textarea_event(e: InputEvent) -> String { let target: HtmlTextAreaElement = event_target.dyn_into().unwrap_throw(); target.value() } +pub fn get_files_from_input_event(e: InputEvent) -> Option { + let event: Event = e.dyn_into().unwrap_throw(); + let event_target = event.target().unwrap_throw(); + let target: HtmlInputElement = event_target.dyn_into().unwrap_throw(); + target.files() +} diff --git a/src/pages/admin/edit.rs b/src/pages/admin/edit.rs index 3d426f7..ebb31a5 100644 --- a/src/pages/admin/edit.rs +++ b/src/pages/admin/edit.rs @@ -228,7 +228,7 @@ pub fn generate_fields_for_project(props: &ProjectFieldsProps) -> Html { <> - + {if (*agent).clone().is_none() { html! {