use log::info; //use log::Level; use reqwasm::http::Request; use serde::{Deserialize, Serialize}; use serde_json; use sycamore::futures::spawn_local; use sycamore::prelude::*; //use sycamore::suspense::Suspense; use sycamore_router::Route; use wasm_bindgen::JsCast; use web_sys::{Event, HtmlInputElement, KeyboardEvent}; // 0.3.5 #[derive(Deserialize, Serialize, Debug, Default, Clone, PartialEq, Hash, Eq)] pub struct BookUI { id: i32, open_library_key: Option, title: String, edition_count: Option, first_publish_year: Option, median_page_count: Option, goodread_id: Option, description: Option, cover: Option, location: Option, time_added: Option, rating: Option, comments: Option, author_name: Option>, person: Option>, place: Option>, subject: Option>, time: Option>, isbn: Option>, } #[derive(Prop, Debug)] pub struct BookUIProp { bookitem: BookUI, } #[derive(Deserialize, Serialize, Debug)] struct PaginatedBookUIList { num_pages: u32, books: Vec, } #[derive(Debug, Default, Clone)] pub struct AppState { pub books: RcSignal>, pub search: RcSignal, pub openlibrary: RcSignal, pub adding: RcSignal, pub updating: RcSignal, pub displaying: RcSignal, pub addingbook: RcSignal, pub displayingbook: RcSignal, } #[derive(Route)] enum AppRoutes { #[to("/")] Home, #[to("/hello-server")] HelloServer, #[not_found] NotFound, } async fn fetch_books(search: String) -> Result, reqwasm::Error> { let url = format!("http://localhost:8081/api/search_openlibrary?search={}", search); let resp = Request::get(&url).send().await?; println!("Fetching books\n"); let body = resp.json::>().await?; Ok(body) } async fn add_book(record: BookUI) -> Result { let url = format!("http://localhost:8081/api/create"); let resp = Request::post(&url).body(serde_wasm_bindgen::to_value(&serde_json::to_string(&record).unwrap()).unwrap()).header("content-type","application/json").send().await?; Ok(resp) } async fn update_book(record: BookUI) -> Result { let url = format!("http://localhost:8081/api/update"); let resp = Request::post(&url).body(serde_wasm_bindgen::to_value(&serde_json::to_string(&record).unwrap()).unwrap()).header("content-type","application/json").send().await?; Ok(resp) } async fn delete_book(id: i32) -> Result { let url = format!("http://localhost:8081/api/delete/{}", id); let resp = Request::get(&url).send().await?; Ok(resp) } async fn list_books(page: u32) -> Result { let url = format!("http://localhost:8081/api/list?page={}",page); let resp = Request::get(&url).send().await?; println!("Fetching books\n"); let body = resp.json::().await?; Ok(body) } #[component] pub fn Header(cx: Scope) -> View { let app_state = use_context::(cx); let value = create_signal(cx, String::new()); let input_ref = create_node_ref(cx); let handle_submit = |event: Event| { let event: KeyboardEvent = event.unchecked_into(); if event.key() == "Enter" { let mut task = value.get().as_ref().clone(); task = task.trim().to_string(); if !task.is_empty() { app_state.search.set(task); app_state.openlibrary.set(true); info!("Fetching search\n"); value.set("".to_string()); input_ref .get::() .unchecked_into::() .set_value(""); } } }; let click_listall = |_| (app_state.openlibrary.set(false)); let click_addbook = |_| { app_state.adding.set(true); app_state.addingbook.set(BookUI::default()); }; view! { cx, header(class="header") { h1 { "Search OpenLibrary" } input(ref=input_ref, class="new-todo", placeholder="Search openlibrary", bind:value=value, on:keyup=handle_submit, ) button(on:click=click_listall) { "List All DB" } button(on:click=click_addbook) { "+ Add New" } } } } #[component] async fn ListDB(cx: Scope<'_>) -> View { let app_state = use_context::(cx); create_effect(cx, || { let app_state = app_state.clone(); if *app_state.openlibrary.get() == false { spawn_local(async move { app_state.books.set( list_books(1) .await .unwrap().books, ) }); } }); let docs = create_memo(cx, || app_state.books.get().iter().cloned().collect::>()); view! {cx, p { (if *app_state.openlibrary.get() == false { view!{ cx, ul { Keyed( iterable=docs, view=|cx, x| view! { cx, BookDB(bookitem=x) }, key =|x| x.id ) } } } else { view!{cx,""} } ) } } } #[component(inline_props)] pub fn BookDB(cx: Scope, bookitem: BookUIProp) -> View { let app_state = use_context::(cx); let book = bookitem.bookitem.clone(); let bookdelete = bookitem.bookitem.clone(); let coverurl = book.cover.clone().unwrap_or("http://localhost:8081/images/placeholder.jpg".to_string()); let handle_delete = move |_| { spawn_local(async move { let temp = delete_book(book.id).await.unwrap(); println!("{}",temp.status()); }); }; let handle_update = move |_| { app_state.adding.set(false); app_state.updating.set(true); app_state.addingbook.set(bookdelete.clone()); }; view! { cx, div(class="column"){ div(class="card"){ img(src=coverurl,width="100") (format!("{:?}",book)) button(class="delete", on:click=handle_delete){ "-" } button(class="update", on:click=handle_update){ "=" } } } } } #[component] async fn ListOL(cx: Scope<'_>) -> View { let app_state = use_context::(cx); create_effect(cx, || { //info!( // "The state changed. New value: {} {}", // app_state.search.get(), //); let app_state = app_state.clone(); app_state.search.track(); spawn_local(async move { if *app_state.search.get() != "" { app_state.books.set( fetch_books(app_state.search.get().to_string()) .await .unwrap(), ) } }); }); let docs = create_memo(cx, || app_state.books.get().iter().cloned().collect::>()); view! {cx, p { (if *app_state.openlibrary.get() == true { view!{ cx, div(class="row") { Keyed( iterable=docs, view=move |cx, x| view! { cx, BookOL(bookitem=x) }, key =|x| x.id ) } } } else { view!{cx,""} } ) } } } #[component(inline_props)] pub fn BookOL(cx: Scope, bookitem: BookUIProp) -> View { let book = bookitem.bookitem.clone(); let bookdisp=book.clone(); let coverurl = book.cover.clone().unwrap_or("http://localhost:8081/images/placeholder.jpg".to_string()); let app_state = use_context::(cx); let handle_add = move |_| { app_state.adding.set(true); app_state.addingbook.set(book.clone()); }; view! { cx, div(class="column"){ div(class="card"){ img(src=coverurl,width="100") (format!("{:?}",bookdisp)) button(class="add", on:click=handle_add){ "+" } } } } } #[component] async fn AddingUI(cx: Scope<'_>) -> View { let app_state = use_context::(cx); let inp_title = create_signal(cx, (*app_state.addingbook.get()).clone().title); let inp_olkey = create_signal(cx, (*app_state.addingbook.get()).clone().open_library_key.unwrap_or("".to_string())); let inp_editioncount = create_signal(cx, (*app_state.addingbook.get()).clone().edition_count.unwrap_or(0).to_string()); let inp_publishyear = create_signal(cx, (*app_state.addingbook.get()).clone().first_publish_year.unwrap_or(0).to_string()); let inp_medianpage = create_signal(cx, (*app_state.addingbook.get()).clone().median_page_count.unwrap_or(0).to_string()); let inp_goodread = create_signal(cx, (*app_state.addingbook.get()).clone().goodread_id.unwrap_or("".to_string())); let inp_desc = create_signal(cx, (*app_state.addingbook.get()).clone().description.unwrap_or("".to_string())); let inp_cover = create_signal(cx, (*app_state.addingbook.get()).clone().cover.unwrap_or("".to_string())); let inp_location = create_signal(cx, (*app_state.addingbook.get()).clone().location.unwrap_or("".to_string())); let inp_rating = create_signal(cx, (*app_state.addingbook.get()).clone().location.unwrap_or("".to_string())); let inp_comments = create_signal(cx, (*app_state.addingbook.get()).clone().comments.unwrap_or("".to_string())); let inp_author = create_signal(cx, (*app_state.addingbook.get()).clone().author_name.unwrap_or(vec!["".to_string()]).join(", ")); let inp_person = create_signal(cx, (*app_state.addingbook.get()).clone().person.unwrap_or(vec!["".to_string()]).join(", ")); let inp_place = create_signal(cx, (*app_state.addingbook.get()).clone().place.unwrap_or(vec!["".to_string()]).join(", ")); let inp_subject = create_signal(cx, (*app_state.addingbook.get()).clone().subject.unwrap_or(vec!["".to_string()]).join(", ")); let inp_time = create_signal(cx, (*app_state.addingbook.get()).clone().time.unwrap_or(vec!["".to_string()]).join(", ")); let inp_isbn = create_signal(cx, (*app_state.addingbook.get()).clone().isbn.unwrap_or(vec!["".to_string()]).join(", ")); create_effect(cx, || { info!("{:?}",*app_state.addingbook.get()); inp_title.set((*app_state.addingbook.get()).clone().title); inp_olkey.set((*app_state.addingbook.get()).clone().open_library_key.unwrap_or("".to_string())); inp_editioncount.set((*app_state.addingbook.get()).clone().edition_count.unwrap_or(0).to_string()); inp_publishyear.set((*app_state.addingbook.get()).clone().first_publish_year.unwrap_or(0).to_string()); inp_medianpage.set((*app_state.addingbook.get()).clone().median_page_count.unwrap_or(0).to_string()); inp_goodread.set((*app_state.addingbook.get()).clone().goodread_id.unwrap_or("".to_string())); inp_desc.set((*app_state.addingbook.get()).clone().description.unwrap_or("".to_string())); inp_cover.set((*app_state.addingbook.get()).clone().cover.unwrap_or("".to_string())); inp_location.set((*app_state.addingbook.get()).clone().location.unwrap_or("".to_string())); inp_rating.set((*app_state.addingbook.get()).clone().location.unwrap_or("".to_string())); inp_comments.set((*app_state.addingbook.get()).clone().comments.unwrap_or("".to_string())); inp_author.set((*app_state.addingbook.get()).clone().author_name.unwrap_or(vec!["".to_string()]).join(", ")); inp_person.set((*app_state.addingbook.get()).clone().person.unwrap_or(vec!["".to_string()]).join(", ")); inp_place.set((*app_state.addingbook.get()).clone().place.unwrap_or(vec!["".to_string()]).join(", ")); inp_subject.set((*app_state.addingbook.get()).clone().subject.unwrap_or(vec!["".to_string()]).join(", ")); inp_time.set((*app_state.addingbook.get()).clone().time.unwrap_or(vec!["".to_string()]).join(", ")); inp_isbn.set((*app_state.addingbook.get()).clone().isbn.unwrap_or(vec!["".to_string()]).join(", ")); }); let handle_add = |_| { info!("Adding book"); let authors: Vec = (*inp_author.get()).clone().split(",").map(str::to_string).collect::>(); let persons: Vec = (*inp_person.get()).clone().split(",").map(str::to_string).collect::>(); let places: Vec = (*inp_place.get()).clone().split(",").map(|x| x.to_string()).collect::>(); let subjects: Vec = (*inp_subject.get()).clone().split(",").map(|x| x.to_string()).collect::>(); let times: Vec = (*inp_time.get()).clone().split(",").map(|x| x.to_string()).collect::>(); let isbns: Vec = (*inp_isbn.get()).clone().split(",").map(|x| x.to_string()).collect::>(); let record : BookUI = BookUI{ id: app_state.addingbook.get().id.clone(), title: (*inp_title.get()).clone(), open_library_key: Some((*inp_olkey.get()).clone()), edition_count: Some(inp_editioncount.get().parse::().unwrap_or(0)), first_publish_year: Some(inp_publishyear.get().parse::().unwrap_or(0)), median_page_count: Some(inp_medianpage.get().parse::().unwrap_or(0)), goodread_id: Some((*inp_goodread.get()).clone()), description: Some((*inp_desc.get()).clone()), cover: Some((*inp_cover.get()).clone()), location: Some((*inp_location.get()).clone()), time_added: Some("NA".to_string()), rating: Some(inp_rating.get().parse::().unwrap_or(0)), comments: Some((*inp_comments.get()).clone()), author_name: Some(authors), person: Some(persons), place: Some(places), subject: Some(subjects), time: Some(times), isbn: Some(isbns), }; if *app_state.updating.get() == false { spawn_local(async move { let temp = add_book(record).await.unwrap(); println!("{}",temp.status()); }); } else { spawn_local(async move { let temp = update_book(record).await.unwrap(); println!("{}",temp.status()); }); } app_state.addingbook.set(BookUI::default()); app_state.updating.set(false); app_state.adding.set(false); }; view! {cx, p { (if *app_state.adding.get() == true || *app_state.updating.get() == true { view!{ cx, input(bind:value=inp_title, placeholder="Title" ) input(bind:value=inp_olkey, placeholder="OpenLibrary Key" ) input(bind:value=inp_editioncount, placeholder="Number of editions" ) input(bind:value=inp_publishyear, placeholder="First publish year" ) input(bind:value=inp_medianpage, placeholder="Page count" ) input(bind:value=inp_goodread, placeholder="GoodRead ID" ) input(bind:value=inp_desc, placeholder="Description" ) input(bind:value=inp_cover, placeholder="Cover URL" ) input(bind:value=inp_location, placeholder="Location" ) input(bind:value=inp_rating, placeholder="Rating (/10)" ) input(bind:value=inp_comments, placeholder="Comments" ) input(bind:value=inp_author, placeholder="Authors") input(bind:value=inp_person, placeholder="Persons" ) input(bind:value=inp_place, placeholder="Places" ) input(bind:value=inp_subject, placeholder="Subjects" ) input(bind:value=inp_time, placeholder="Times" ) input(bind:value=inp_isbn, placeholder="ISBNs" ) button(class="add", on:click=handle_add){ "Add book to DB" } } } else { view!{cx,""} } ) } } } #[component] fn App(cx: Scope) -> View { let app_state = AppState { books: create_rc_signal(Vec::new()), search: create_rc_signal(String::default()), openlibrary: create_rc_signal(bool::default()), adding: create_rc_signal(bool::default()), updating: create_rc_signal(bool::default()), addingbook: create_rc_signal(BookUI::default()), displaying: create_rc_signal(bool::default()), displayingbook: create_rc_signal(BookUI::default()), }; provide_context(cx, app_state); view! { cx, div { Header {} AddingUI{} ListOL {} ListDB{} } } } fn main() { console_error_panic_hook::set_once(); console_log::init_with_level(log::Level::Debug).unwrap(); sycamore::render(|cx| view! { cx, App {} }); }