diff --git a/.gitignore b/.gitignore index ad547b6..a2868de 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,7 @@ playwright/.cache/ # Sass cache dir .sass-cache/ -.idea/ \ No newline at end of file +.idea/ + +# Ignore database file +compareware.db \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9f44832..b9ca793 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags", + "bitflags 2.6.0", "bytes", "futures-core", "futures-sink", @@ -29,7 +29,7 @@ dependencies = [ "actix-service", "actix-utils", "actix-web", - "bitflags", + "bitflags 2.6.0", "bytes", "derive_more", "futures-core", @@ -52,9 +52,9 @@ dependencies = [ "actix-rt", "actix-service", "actix-utils", - "ahash", + "ahash 0.8.11", "base64", - "bitflags", + "bitflags 2.6.0", "brotli", "bytes", "bytestring", @@ -127,7 +127,7 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio", + "mio 1.0.3", "socket2", "tokio", "tracing", @@ -169,7 +169,7 @@ dependencies = [ "actix-service", "actix-utils", "actix-web-codegen", - "ahash", + "ahash 0.8.11", "bytes", "bytestring", "cfg-if", @@ -233,6 +233,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -398,7 +409,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -510,6 +521,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -729,7 +746,10 @@ dependencies = [ "leptos_actix", "leptos_meta", "leptos_router", + "mio 0.8.11", "nostr-sdk", + "paste", + "rusqlite", "secp256k1 0.30.0", "serde", "serde_json", @@ -982,6 +1002,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "flate2" version = "1.0.35" @@ -1227,6 +1259,15 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.13.2" @@ -1239,7 +1280,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", ] @@ -1254,6 +1295,15 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown 0.11.2", +] + [[package]] name = "hex-conservative" version = "0.1.2" @@ -1810,6 +1860,16 @@ version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +[[package]] +name = "libsqlite3-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "linear-map" version = "1.2.0" @@ -1937,6 +1997,18 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.3" @@ -2109,7 +2181,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -2380,7 +2452,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -2447,6 +2519,21 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "rusqlite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" +dependencies = [ + "bitflags 1.3.2", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2990,7 +3077,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.3", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -3284,6 +3371,12 @@ version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -3414,13 +3507,22 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3429,7 +3531,22 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -3438,28 +3555,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3472,24 +3607,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index d5a1264..3794533 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,9 @@ leptos = { version = "0.6" } leptos_meta = { version = "0.6" } leptos_actix = { version = "0.6", optional = true } leptos_router = { version = "0.6" } +paste = "1.0" wasm-bindgen = "=0.2.99" +rusqlite = { version = "0.27.0", optional = true} serde = { version = "1.0", features = ["derive"] } uuid = { version = "1.0", features = ["v4"] } web-sys = { version = "0.3", features = ["Event"] } @@ -28,8 +30,10 @@ wasm-bindgen-futures = "0.4" serde_json="1.0.133" thiserror = "2.0.9" zerofrom = "0.1" +mio = "0.8" [features] +default = ["ssr"] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] ssr = [ @@ -39,6 +43,7 @@ ssr = [ "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", + "dep:rusqlite" ] # Override secp256k1's default features diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..de3d6b7 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,65 @@ +#[cfg(feature = "ssr")] +use actix_web::{web, HttpResponse}; +#[cfg(feature = "ssr")] +use crate::db::{Database, DbItem}; +#[cfg(feature = "ssr")] +use std::sync::Arc; +#[cfg(feature = "ssr")] +use tokio::sync::Mutex; + +#[cfg(feature = "ssr")] +pub async fn get_items(db: web::Data>>) -> HttpResponse { + let db = db.lock().await; + match db.get_items().await { + Ok(items) => HttpResponse::Ok().json(items), + Err(err) => { + leptos::logging::error!("Failed to fetch items: {:?}", err); + HttpResponse::InternalServerError().body("Failed to fetch items") + } + } +} + +#[cfg(feature = "ssr")] +pub async fn create_item( + db: web::Data>>, + item: web::Json, +) -> HttpResponse { + let db = db.lock().await; + match db.insert_item(&item.into_inner()).await { + Ok(_) => HttpResponse::Ok().body("Item inserted"), + Err(err) => { + leptos::logging::error!("Failed to insert item: {:?}", err); + HttpResponse::InternalServerError().body("Failed to insert item") + } + } +} + +#[cfg(feature = "ssr")] +pub async fn delete_item( + db: web::Data>>, + item_id: web::Path, +) -> HttpResponse { + let db = db.lock().await; + match db.delete_item(&item_id).await { + Ok(_) => HttpResponse::Ok().body("Item deleted"), + Err(err) => { + leptos::logging::error!("Failed to delete item: {:?}", err); + HttpResponse::InternalServerError().body("Failed to delete item") + } + } +} + +#[cfg(feature = "ssr")] +pub async fn delete_property( + db: web::Data>>, + property: web::Path, +) -> HttpResponse { + let db = db.lock().await; + match db.delete_property(&property).await { + Ok(_) => HttpResponse::Ok().body("Property deleted"), + Err(err) => { + leptos::logging::error!("Failed to delete property: {:?}", err); + HttpResponse::InternalServerError().body("Failed to delete property") + } + } +} \ No newline at end of file diff --git a/src/components/items_list.rs b/src/components/items_list.rs index 53aae06..1e8a754 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -1,7 +1,7 @@ use crate::components::editable_cell::EditableCell; use crate::components::editable_cell::InputType; use leptos::*; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use uuid::Uuid; use leptos::logging::log; use crate::models::item::Item; @@ -16,11 +16,24 @@ struct WikidataSuggestion { description: Option, } + +#[derive(Deserialize, Debug)] +struct DbItem { + id: String, + name: String, + description: String, + wikidata_id: Option, + custom_properties: String, +} + #[component] pub fn ItemsList( items: ReadSignal>, set_items: WriteSignal>, ) -> impl IntoView { + // State to track selected properties + let (selected_properties, set_selected_properties) = create_signal(HashMap::::new()); + // State to track the currently focused cell let (focused_cell, set_focused_cell) = create_signal(None::); @@ -33,8 +46,67 @@ pub fn ItemsList( // cache to store fetched properties let (fetched_properties, set_fetched_properties) = create_signal(HashMap::::new()); - //signal to store the fetched property labels + // Signal to store the fetched property labels let (property_labels, set_property_labels) = create_signal(HashMap::::new()); + + spawn_local(async move { + match load_items_from_db().await { + Ok(loaded_items) => { + // Set the loaded items + if loaded_items.is_empty() { + // Initialize with one empty item if the database is empty + set_items.set(vec![Item { + id: Uuid::new_v4().to_string(), + name: String::new(), + description: String::new(), + wikidata_id: None, + custom_properties: HashMap::new(), + }]); + } else { + set_items.set(loaded_items.clone()); + } + + // Derive selected properties from the loaded items + let mut selected_props = HashMap::new(); + let loaded_items_clone = loaded_items.clone(); + for item in loaded_items { + for (property, _) in item.custom_properties { + selected_props.insert(property, true); + } + } + set_selected_properties.set(selected_props); + + // Update the custom_properties signal + let mut custom_props = Vec::new(); + for item in loaded_items_clone { + for (property, _) in &item.custom_properties { + if !custom_props.iter().any(|p| p == property) { + custom_props.push(property.clone()); + } + } + } + + let custom_props_clone = custom_props.clone(); + set_custom_properties.set(custom_props); + + // Fetch labels for the custom properties + let property_ids = custom_props_clone; + let labels = fetch_property_labels(property_ids).await; + set_property_labels.update(|labels_map| { + for (key, value) in labels { + labels_map.insert(key, value); + } + }); + + log!("Items after loading: {:?}", items.get()); + } + Err(err) => { + log!("Error loading items: {}", err); + } + } + }); + + // Ensure there's an initial empty row if items.get().is_empty() { set_items.set(vec![Item { @@ -46,6 +118,153 @@ pub fn ItemsList( custom_properties: HashMap::new(), }]); } + + // Function to send an item to the backend API + async fn save_item_to_db(item: Item, selected_properties: ReadSignal>) { + // Use a reactive closure to access `selected_properties` + let custom_properties: HashMap = (move || { + let selected_props = selected_properties.get(); // Access the signal inside a reactive closure + item.custom_properties + .into_iter() + .filter(|(key, _)| selected_props.contains_key(key)) // Use the extracted value + .collect() + })(); + + // Serialize `custom_properties` to a JSON string + let custom_properties = serde_json::to_string(&custom_properties).unwrap(); + + // Create a new struct to send to the backend + #[derive(Serialize, Debug)] + struct ItemToSend { + id: String, + name: String, + description: String, + wikidata_id: Option, + custom_properties: String, // JSON-encoded string + } + + let item_to_send = ItemToSend { + id: item.id, + name: item.name, + description: item.description, + wikidata_id: item.wikidata_id, + custom_properties, // Use the serialized string + }; + + let response = gloo_net::http::Request::post("/api/items") + .json(&item_to_send) + .unwrap() + .send() + .await; + + match response { + Ok(resp) => { + if resp.status() == 200 { + log!("Item saved to database: {:?}", item_to_send); + } else { + log!("Failed to save item: {}", resp.status_text()); + } + } + Err(err) => log!("Failed to save item: {:?}", err), + } + } + + //function to load items from database + async fn load_items_from_db() -> Result, String> { + let response = gloo_net::http::Request::get("/api/items") + .send() + .await + .map_err(|err| format!("Failed to fetch items: {:?}", err))?; + + if response.status() == 200 { + // Deserialize into Vec + log!("Loading items from DB..."); + let db_items = response + .json::>() + .await + .map_err(|err| format!("Failed to parse items: {:?}", err))?; + + log!("Deserialized DB items: {:?}", db_items); + + // Convert DbItem to Item + let items = db_items + .into_iter() + .map(|db_item| { + // Deserialize `custom_properties` from a JSON string to a HashMap + let custom_properties: HashMap = + serde_json::from_str(&db_item.custom_properties) + .unwrap_or_default(); // Fallback to an empty HashMap if deserialization fails + + log!("Loaded item: {:?}", db_item.id); + log!("Custom properties: {:?}", custom_properties); + + Item { + id: db_item.id, + name: db_item.name, + description: db_item.description, + wikidata_id: db_item.wikidata_id, + custom_properties, // Deserialized HashMap + } + }) + .collect(); + log!("Converted items: {:?}", items); + Ok(items) + } else { + Err(format!("Failed to fetch items: {}", response.status_text())) + } + } + + // remove an item + let remove_item = move |index: usize| { + let item_id = items.get()[index].id.clone(); + spawn_local(async move { + let response = gloo_net::http::Request::delete(&format!("/api/items/{}", item_id)) + .send() + .await; + match response { + Ok(resp) => { + if resp.status() == 200 { + set_items.update(|items| { + items.remove(index); + }); + log!("Item deleted: {}", item_id); + } else { + log!("Failed to delete item: {}", resp.status_text()); + } + } + Err(err) => log!("Failed to delete item: {:?}", err), + } + }); + }; + + let remove_property = move |property: String| { + spawn_local(async move { + let response = gloo_net::http::Request::delete(&format!("/api/properties/{}", property)) + .send() + .await; + match response { + Ok(resp) => { + if resp.status() == 200 { + set_custom_properties.update(|props| { + props.retain(|p| p != &property); + }); + set_selected_properties.update(|selected| { + selected.remove(&property); + }); + set_items.update(|items| { + for item in items { + item.custom_properties.remove(&property); + } + }); + log!("Property deleted: {}", property); + } else { + log!("Failed to delete property: {}", resp.status_text()); + } + } + Err(err) => log!("Failed to delete property: {:?}", err), + } + }); + }; let (wikidata_suggestions, set_wikidata_suggestions) = create_signal(HashMap::>::new()); @@ -164,10 +383,22 @@ pub fn ItemsList( set_custom_properties.update(|props| { if !props.contains(&property) && !property.is_empty() { props.push(property.clone()); + + //update the selected_properties state when a new property is added + set_selected_properties.update(|selected| { + selected.insert(property.clone(), true); + }); + // Ensure the grid updates reactively set_items.update(|items| { for item in items { item.custom_properties.entry(property.clone()).or_insert_with(|| "".to_string()); + + // Save the updated item to the database + let item_clone = item.clone(); + spawn_local(async move { + save_item_to_db(item_clone, selected_properties).await; + }); } }); @@ -193,7 +424,7 @@ pub fn ItemsList( let property_clone = property.clone(); spawn_local(async move { let properties = fetch_item_properties(&wikidata_id, set_fetched_properties, set_property_labels).await; - log!("Fetched properties for Wikidata ID {}: {:?}", wikidata_id, properties); + // log!("Fetched properties for Wikidata ID {}: {:?}", wikidata_id, properties); if let Some(value) = properties.get(&property_clone) { set_items.update(|items| { if let Some(item) = items.iter_mut().find(|item| item.wikidata_id.as_ref().unwrap() == &wikidata_id) { @@ -237,31 +468,34 @@ pub fn ItemsList( item.custom_properties.insert(field.to_string(), value.clone()); } } - } + // Save the updated item to the database + let item_clone = item.clone(); + spawn_local(async move { + save_item_to_db(item_clone, selected_properties).await; + }); + } // Automatically add a new row when editing the last row if index == items.len() - 1 && !value.is_empty() { - items.push(Item { + let new_item = Item { id: Uuid::new_v4().to_string(), name: String::new(), description: String::new(), // reviews: vec![], wikidata_id: None, custom_properties: HashMap::new(), + }; + items.push(new_item.clone()); + + // Save the new item to the database + spawn_local(async move { + save_item_to_db(new_item, selected_properties).await; }); } log!("Items updated: {:?}", items); }); }; - - // Remove an item - let remove_item = move |index: usize| { - set_items.update(|items| { - items.remove(index); - }); - }; - // List of properties to display as rows let properties = vec!["Name", "Description", "Actions"]; @@ -360,7 +594,7 @@ pub fn ItemsList( let set_property_labels = set_property_labels.clone(); spawn_local(async move { let properties = fetch_item_properties(&wikidata_id, set_fetched_properties, set_property_labels).await; - log!("Fetched properties for Wikidata ID {}: {:?}", wikidata_id, properties); + // log!("Fetched properties for Wikidata ID {}: {:?}", wikidata_id, properties); // Populate the custom properties for the new item set_items.update(|items| { @@ -426,36 +660,56 @@ pub fn ItemsList( // Dynamically adding custom properties as columns {move || { let custom_props = custom_properties.get().clone(); + log!("Rendering custom properties: {:?}", custom_props); custom_props.into_iter().map(move |property| { let property_clone = property.clone(); let property_label = property_labels.get().get(&property_clone).cloned().unwrap_or_else(|| property_clone.clone()); + let property_clone_for_button = property_clone.clone(); + let property_clone_for_cells = property_clone.clone(); view! { - { property_label } + + { property_label } + + {move || { - let property_clone = property_clone.clone(); // Clone `property_clone` again for the inner closure + let property_clone_for_cells = property_clone.clone(); items.get().iter().enumerate().map(move |(index, item)| { - let property_clone_for_closure = property_clone.clone(); - view! { - - - - } - }).collect::>() - }} + let property_clone_for_closure = property_clone_for_cells.clone(); + view! { + + + + } + }).collect::>()} + } } }).collect::>() diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..16fe958 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,136 @@ +#[cfg(feature = "ssr")] +mod db_impl { + use rusqlite::{Connection, Error}; + use serde::{Deserialize, Serialize}; + use std::sync::Arc; + use tokio::sync::Mutex; + use leptos::logging; + + // Define a struct to represent a database connection + #[derive(Debug)] + pub struct Database { + conn: Arc>, + } + + impl Database { + // Create a new database connection + pub fn new(db_path: &str) -> Result { + let conn = Connection::open(db_path)?; + logging::log!("Database connection established at: {}", db_path); + Ok(Database { + conn: Arc::new(Mutex::new(conn)), + }) + } + + // Create the database schema + pub async fn create_schema(&self) -> Result<(), Error> { + let conn = self.conn.lock().await; + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS items ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + wikidata_id TEXT, + custom_properties TEXT + );", + )?; + logging::log!("Database schema created or verified"); + Ok(()) + } + + // Insert a new item into the database + pub async fn insert_item(&self, item: &DbItem) -> Result<(), Error> { + let conn = self.conn.lock().await; + let wikidata_id = item.wikidata_id.as_ref().map(|s| s.as_str()).unwrap_or(""); + conn.execute( + "INSERT INTO items (id, name, description, wikidata_id, custom_properties) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + description = excluded.description, + wikidata_id = excluded.wikidata_id, + custom_properties = excluded.custom_properties;", + &[ + &item.id, + &item.name, + &item.description, + &wikidata_id.to_string(), + &item.custom_properties, + ], + )?; + logging::log!("Item inserted: {}", item.id); + Ok(()) + } + + pub async fn delete_item(&self, item_id: &str) -> Result<(), Error> { + let conn = self.conn.lock().await; + conn.execute("DELETE FROM items WHERE id = ?", &[item_id])?; + logging::log!("Item deleted: {}", item_id); + Ok(()) + } + + pub async fn delete_property(&self, property: &str) -> Result<(), Error> { + let conn = self.conn.lock().await; + let query = format!("UPDATE items SET custom_properties = json_remove(custom_properties, '$.{}')", property); + conn.execute(&query, []).map_err(|e| Error::from(e))?; + logging::log!("Property deleted: {}", property); + Ok(()) + } + + // Retrieve all items from the database + pub async fn get_items(&self) -> Result, Error> { + let conn = self.conn.lock().await; + let mut stmt = conn.prepare("SELECT * FROM items;")?; + let items = stmt.query_map([], |row| { + Ok(DbItem { + id: row.get(0)?, + name: row.get(1)?, + description: row.get(2)?, + wikidata_id: row.get(3)?, + custom_properties: row.get(4)?, + }) + })?; + let mut result = Vec::new(); + for item in items { + result.push(item?); + } + logging::log!("Fetched {} items from the database", result.len()); // Log with Leptos + Ok(result) + } + } + + // Define a struct to represent an item in the database + #[derive(Debug, Deserialize, Serialize, Clone)] + pub struct DbItem { + pub id: String, + pub name: String, + pub description: String, + pub wikidata_id: Option, + pub custom_properties: String, + } + + // Implement conversion from DbItem to a JSON-friendly format + #[derive(Debug, Deserialize, Serialize, Clone)] + pub struct ItemResponse { + pub id: String, + pub name: String, + pub description: String, + pub wikidata_id: Option, + pub custom_properties: String, + } + + impl From for ItemResponse { + fn from(item: DbItem) -> Self { + ItemResponse { + id: item.id, + name: item.name, + description: item.description, + wikidata_id: item.wikidata_id, + custom_properties: item.custom_properties, + } + } + } +} + +#[cfg(feature = "ssr")] +pub use db_impl::{Database, DbItem, ItemResponse}; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 10f7ef7..a2a7cab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,9 @@ pub mod app; pub mod components; pub mod models; pub mod nostr; +pub mod api; +#[cfg(feature = "ssr")] +pub mod db; #[cfg(feature = "hydrate")] diff --git a/src/main.rs b/src/main.rs index 535b829..b2a782a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,27 +6,55 @@ async fn main() -> std::io::Result<()> { use leptos::*; use leptos_actix::{generate_route_list, LeptosRoutes}; use compareware::app::*; + use compareware::db::{Database, DbItem}; + use compareware::api::{get_items, create_item, delete_item, delete_property}; // Import API handlers + use std::sync::Arc; + use tokio::sync::Mutex; + // Load configuration let conf = get_configuration(None).await.unwrap(); let addr = conf.leptos_options.site_addr; + + // Initialize the database + let db = Database::new("compareware.db").unwrap(); + db.create_schema().await.unwrap(); // Ensure the schema is created + let db = Arc::new(Mutex::new(db)); // Wrap the database in an Arc> for shared state + // Generate the list of routes in your Leptos App let routes = generate_route_list(App); println!("listening on http://{}", &addr); + // Start the Actix Web server HttpServer::new(move || { let leptos_options = &conf.leptos_options; let site_root = &leptos_options.site_root; + let db = db.clone(); // Clone the Arc for each worker + App::new() - // serve JS/WASM/CSS from `pkg` + // Register custom API routes BEFORE Leptos server functions + .service( + web::scope("/api") + .route("/items", web::get().to(get_items)) // GET /api/items + .route("/items", web::post().to(create_item)) // POST /api/items + .route("/items/{id}", web::delete().to(delete_item)) // DELETE /api/items/{id} + .route("/properties/{property}", web::delete().to(delete_property)), // DELETE /api/properties/{property} + ) + // Register server functions + .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) + // Serve JS/WASM/CSS from `pkg` .service(Files::new("/pkg", format!("{site_root}/pkg"))) - // serve other assets from the `assets` directory + // Serve other assets from the `assets` directory .service(Files::new("/assets", site_root)) - // serve the favicon from /favicon.ico + // Serve the favicon from /favicon.ico .service(favicon) + // Register Leptos routes .leptos_routes(leptos_options.to_owned(), routes.to_owned(), App) + // Pass Leptos options to the app .app_data(web::Data::new(leptos_options.to_owned())) - //.wrap(middleware::Compress::default()) + //.wrap(middleware::Compress::default()) + // Pass the database as shared state + .app_data(web::Data::new(db)) }) .bind(&addr)? .run() @@ -63,4 +91,4 @@ pub fn main() { console_error_panic_hook::set_once(); leptos::mount_to_body(App); -} +} \ No newline at end of file