Compare commits
25 commits
Author | SHA1 | Date | |
---|---|---|---|
fd39e3b967 | |||
ad9942a44f | |||
585a4a6eb7 | |||
e90a6be010 | |||
63aaa57fa1 | |||
a35d4d557d | |||
2bcdea79dc | |||
a379e93f44 | |||
a8d8e9a131 | |||
af1e6d949f | |||
5815c9fe10 | |||
8e3c87f315 | |||
b6b1ebde9c | |||
74bd1a89e5 | |||
9beb997125 | |||
63f11f6a2d | |||
eba20abf5a | |||
ecc991cc24 | |||
8860ace51f | |||
7939c9e7b6 | |||
fddec7f728 | |||
1a5c245250 | |||
e72ed778a2 | |||
bfded464c9 | |||
ce1e93fc49 |
6 changed files with 530 additions and 183 deletions
104
src/api.rs
104
src/api.rs
|
@ -1,16 +1,34 @@
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use actix_web::{web, HttpResponse};
|
use actix_web::{web, HttpResponse};
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use crate::db::{Database, DbItem};
|
use crate::db::Database;
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use crate::models::item::Item;
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use std::collections::HashMap;
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
use leptos::logging::log;
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub async fn get_items(db: web::Data<Arc<Mutex<Database>>>) -> HttpResponse {
|
use serde::{Deserialize, Serialize};
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct ItemRequest {
|
||||||
|
pub url: String,
|
||||||
|
pub item: Item,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub async fn get_items(
|
||||||
|
db: web::Data<Arc<Mutex<Database>>>,
|
||||||
|
url: web::Query<String>,
|
||||||
|
) -> HttpResponse {
|
||||||
let db = db.lock().await;
|
let db = db.lock().await;
|
||||||
match db.get_items().await {
|
match db.get_items_by_url(&url).await {
|
||||||
Ok(items) => HttpResponse::Ok().json(items),
|
Ok(items) => HttpResponse::Ok().json(items),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
leptos::logging::error!("Failed to fetch items: {:?}", err);
|
leptos::logging::error!("Failed to fetch items: {:?}", err);
|
||||||
|
@ -22,14 +40,28 @@ pub async fn get_items(db: web::Data<Arc<Mutex<Database>>>) -> HttpResponse {
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub async fn create_item(
|
pub async fn create_item(
|
||||||
db: web::Data<Arc<Mutex<Database>>>,
|
db: web::Data<Arc<Mutex<Database>>>,
|
||||||
item: web::Json<DbItem>,
|
request: web::Json<ItemRequest>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let db = db.lock().await;
|
let db = db.lock().await;
|
||||||
match db.insert_item(&item.into_inner()).await {
|
let url = request.url.clone();
|
||||||
Ok(_) => HttpResponse::Ok().body("Item inserted"),
|
let item = request.item.clone();
|
||||||
Err(err) => {
|
let item_id = request.item.id.clone();
|
||||||
leptos::logging::error!("Failed to insert item: {:?}", err);
|
// request logging
|
||||||
HttpResponse::InternalServerError().body("Failed to insert item")
|
log!("[API] Received item request - URL: {}, Item ID: {}",
|
||||||
|
request.url, request.item.id);
|
||||||
|
|
||||||
|
// raw JSON logging
|
||||||
|
let raw_json = serde_json::to_string(&request.into_inner()).unwrap();
|
||||||
|
log!("[API] Raw request JSON: {}", raw_json);
|
||||||
|
|
||||||
|
match db.insert_item_by_url(&url, &item).await {
|
||||||
|
Ok(_) => {
|
||||||
|
log!("[API] Successfully saved item ID: {}", item_id);
|
||||||
|
HttpResponse::Ok().json(item)
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log!("[API] Database error: {:?}", e);
|
||||||
|
HttpResponse::BadRequest().body(format!("Database error: {}", e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,10 +69,11 @@ pub async fn create_item(
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub async fn delete_item(
|
pub async fn delete_item(
|
||||||
db: web::Data<Arc<Mutex<Database>>>,
|
db: web::Data<Arc<Mutex<Database>>>,
|
||||||
|
url: web::Query<String>,
|
||||||
item_id: web::Path<String>,
|
item_id: web::Path<String>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let db = db.lock().await;
|
let db = db.lock().await;
|
||||||
match db.delete_item(&item_id).await {
|
match db.delete_item_by_url(&url, &item_id).await {
|
||||||
Ok(_) => HttpResponse::Ok().body("Item deleted"),
|
Ok(_) => HttpResponse::Ok().body("Item deleted"),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
leptos::logging::error!("Failed to delete item: {:?}", err);
|
leptos::logging::error!("Failed to delete item: {:?}", err);
|
||||||
|
@ -52,14 +85,63 @@ pub async fn delete_item(
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub async fn delete_property(
|
pub async fn delete_property(
|
||||||
db: web::Data<Arc<Mutex<Database>>>,
|
db: web::Data<Arc<Mutex<Database>>>,
|
||||||
|
url: web::Query<String>,
|
||||||
property: web::Path<String>,
|
property: web::Path<String>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let db = db.lock().await;
|
let db = db.lock().await;
|
||||||
match db.delete_property(&property).await {
|
match db.delete_property_by_url(&url, &property).await {
|
||||||
Ok(_) => HttpResponse::Ok().body("Property deleted"),
|
Ok(_) => HttpResponse::Ok().body("Property deleted"),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
leptos::logging::error!("Failed to delete property: {:?}", err);
|
leptos::logging::error!("Failed to delete property: {:?}", err);
|
||||||
HttpResponse::InternalServerError().body("Failed to delete property")
|
HttpResponse::InternalServerError().body("Failed to delete property")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub async fn get_items_by_url(
|
||||||
|
db: web::Data<Arc<Mutex<Database>>>,
|
||||||
|
query: web::Query<HashMap<String, String>>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let url = query.get("url").unwrap_or(&String::new()).to_string();
|
||||||
|
let db = db.lock().await;
|
||||||
|
match db.get_items_by_url(&url).await {
|
||||||
|
Ok(items) => HttpResponse::Ok().json(items),
|
||||||
|
Err(err) => {
|
||||||
|
leptos::logging::error!("Failed to fetch items by URL: {:?}", err);
|
||||||
|
HttpResponse::InternalServerError().body("Failed to fetch items by URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub async fn delete_item_by_url(
|
||||||
|
db: web::Data<Arc<Mutex<Database>>>,
|
||||||
|
url: web::Path<String>,
|
||||||
|
item_id: web::Path<String>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let db = db.lock().await;
|
||||||
|
match db.delete_item_by_url(&url, &item_id).await {
|
||||||
|
Ok(_) => HttpResponse::Ok().body("Item deleted"),
|
||||||
|
Err(err) => {
|
||||||
|
leptos::logging::error!("Failed to delete item by URL: {:?}", err);
|
||||||
|
HttpResponse::InternalServerError().body("Failed to delete item by URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
pub async fn delete_property_by_url(
|
||||||
|
db: web::Data<Arc<Mutex<Database>>>,
|
||||||
|
url: web::Path<String>,
|
||||||
|
property: web::Path<String>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let db = db.lock().await;
|
||||||
|
match db.delete_property_by_url(&url, &property).await {
|
||||||
|
Ok(_) => HttpResponse::Ok().body("Property deleted"),
|
||||||
|
Err(err) => {
|
||||||
|
leptos::logging::error!("Failed to delete property by URL: {:?}", err);
|
||||||
|
HttpResponse::InternalServerError().body("Failed to delete property by URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
41
src/app.rs
41
src/app.rs
|
@ -1,6 +1,8 @@
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_meta::*;
|
use leptos_meta::*;
|
||||||
use crate::components::items_list::ItemsList;
|
use leptos_router::*;
|
||||||
|
use leptos::logging::log;
|
||||||
|
use crate::components::items_list::{ItemsList, load_items_from_db};
|
||||||
use crate::models::item::Item;
|
use crate::models::item::Item;
|
||||||
use crate::nostr::NostrClient;
|
use crate::nostr::NostrClient;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
@ -26,13 +28,36 @@ pub fn App() -> impl IntoView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Stylesheet href="/assets/style.css" />
|
<Router>
|
||||||
<Stylesheet href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" />
|
<Routes>
|
||||||
<div>
|
<Route path="/:url?" view=move || {
|
||||||
<h1>{ "CompareWare" }</h1>
|
let params = use_params_map();
|
||||||
<ItemsList items=items_signal set_items=set_items />
|
let url = move || params.with(|params| params.get("url").cloned().unwrap_or_default());
|
||||||
</div>
|
|
||||||
|
// This effect will re-run when URL changes
|
||||||
|
create_effect(move |_| {
|
||||||
|
let current_url = url();
|
||||||
|
spawn_local(async move {
|
||||||
|
// Load items for new URL
|
||||||
|
match load_items_from_db(¤t_url).await {
|
||||||
|
Ok(loaded_items) => {
|
||||||
|
set_items.set(loaded_items);
|
||||||
|
}
|
||||||
|
Err(err) => log!("Error loading items: {}", err),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
view! {
|
||||||
|
<Stylesheet href="/assets/style.css" />
|
||||||
|
<Stylesheet href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" />
|
||||||
|
<div>
|
||||||
|
<h1>{ "CompareWare" }</h1>
|
||||||
|
<ItemsList items=items_signal set_items=set_items />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}/>
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,8 @@ use crate::models::item::Item;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use wasm_bindgen::JsCast;
|
use wasm_bindgen::JsCast;
|
||||||
|
use std::rc::Rc;
|
||||||
use urlencoding::encode;
|
use urlencoding::encode;
|
||||||
use gloo_net::http::Request;
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Clone, Debug)]
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
struct WikidataSuggestion {
|
struct WikidataSuggestion {
|
||||||
id: String,
|
id: String,
|
||||||
|
@ -19,14 +17,28 @@ struct WikidataSuggestion {
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//function to load items from database
|
||||||
|
pub async fn load_items_from_db(current_url: &str) -> Result<Vec<Item>, String> {
|
||||||
|
let encoded_url = encode(¤t_url);
|
||||||
|
let api_url = format!("/api/urls/{}/items", encoded_url);
|
||||||
|
let response = gloo_net::http::Request::get(&api_url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|err| format!("Failed to fetch items: {:?}", err))?;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
if response.status() == 200 {
|
||||||
struct DbItem {
|
// Deserialize into Vec<Item>
|
||||||
id: String,
|
log!("Loading items from DB...");
|
||||||
name: String,
|
let items = response
|
||||||
description: String,
|
.json::<Vec<Item>>()
|
||||||
wikidata_id: Option<String>,
|
.await
|
||||||
custom_properties: String,
|
.map_err(|err| format!("Failed to parse items: {:?}", err))?;
|
||||||
|
|
||||||
|
Ok(items)
|
||||||
|
} else {
|
||||||
|
let body = response.text().await.unwrap_or_default();
|
||||||
|
Err(format!("Server error ({}): {}", response.status(), body))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
@ -52,8 +64,29 @@ pub fn ItemsList(
|
||||||
// Signal to store the fetched property labels
|
// Signal to store the fetched property labels
|
||||||
let (property_labels, set_property_labels) = create_signal(HashMap::<String, String>::new());
|
let (property_labels, set_property_labels) = create_signal(HashMap::<String, String>::new());
|
||||||
|
|
||||||
spawn_local(async move {
|
#[cfg(feature = "ssr")]
|
||||||
match load_items_from_db().await {
|
fn get_current_url() -> String {
|
||||||
|
use leptos::use_context;
|
||||||
|
use actix_web::HttpRequest;
|
||||||
|
|
||||||
|
use_context::<HttpRequest>()
|
||||||
|
.map(|req| req.uri().to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "ssr"))]
|
||||||
|
fn get_current_url() -> String {
|
||||||
|
web_sys::window()
|
||||||
|
.and_then(|win| win.location().href().ok())
|
||||||
|
.unwrap_or_else(|| "".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_url = Rc::new(get_current_url());
|
||||||
|
|
||||||
|
spawn_local({
|
||||||
|
let current_url = Rc::clone(¤t_url);
|
||||||
|
async move {
|
||||||
|
match load_items_from_db(¤t_url).await {
|
||||||
Ok(loaded_items) => {
|
Ok(loaded_items) => {
|
||||||
// Set the loaded items
|
// Set the loaded items
|
||||||
if loaded_items.is_empty() {
|
if loaded_items.is_empty() {
|
||||||
|
@ -107,7 +140,7 @@ pub fn ItemsList(
|
||||||
log!("Error loading items: {}", err);
|
log!("Error loading items: {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}});
|
||||||
|
|
||||||
|
|
||||||
// Ensure there's an initial empty row
|
// Ensure there's an initial empty row
|
||||||
|
@ -116,49 +149,54 @@ pub fn ItemsList(
|
||||||
id: Uuid::new_v4().to_string(),
|
id: Uuid::new_v4().to_string(),
|
||||||
name: String::new(),
|
name: String::new(),
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
// reviews: vec![],
|
|
||||||
wikidata_id: None,
|
wikidata_id: None,
|
||||||
custom_properties: HashMap::new(),
|
custom_properties: HashMap::new(),
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to send an item to the backend API
|
// Function to send an item to the backend API
|
||||||
async fn save_item_to_db(item: Item, selected_properties: ReadSignal<HashMap<String, bool>>) {
|
async fn save_item_to_db(item: Item, selected_properties: ReadSignal<HashMap<String, bool>>, current_url: String) {
|
||||||
|
|
||||||
|
let custom_props = item.custom_properties.clone();
|
||||||
// Use a reactive closure to access `selected_properties`
|
// Use a reactive closure to access `selected_properties`
|
||||||
let custom_properties: HashMap<String, String> = (move || {
|
let custom_properties: HashMap<String, String> = (move || {
|
||||||
let selected_props = selected_properties.get(); // Access the signal inside a reactive closure
|
let selected_props = selected_properties.get(); // Access the signal inside a reactive closure
|
||||||
item.custom_properties
|
custom_props
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|(key, _)| selected_props.contains_key(key)) // Use the extracted value
|
.filter(|(key, _)| selected_props.contains_key(key)) // Use the extracted value
|
||||||
.collect()
|
.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
|
// Create a new struct to send to the backend
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
struct ItemToSend {
|
struct ItemRequest {
|
||||||
id: String,
|
url: String,
|
||||||
name: String,
|
item: Item,
|
||||||
description: String,
|
|
||||||
wikidata_id: Option<String>,
|
|
||||||
custom_properties: String, // JSON-encoded string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log!("[FRONTEND] Saving item - ID: {}, Name: '{}', Properties: {:?}",
|
||||||
|
item.id, item.name, item.custom_properties);
|
||||||
|
|
||||||
let item_to_send = ItemToSend {
|
let item_to_send = ItemRequest {
|
||||||
id: item.id,
|
url: current_url.to_string(),
|
||||||
name: item.name,
|
item: Item {
|
||||||
description: item.description,
|
id: item.id,
|
||||||
wikidata_id: item.wikidata_id,
|
name: item.name,
|
||||||
custom_properties, // Use the serialized string
|
description: item.description,
|
||||||
|
wikidata_id: item.wikidata_id,
|
||||||
|
custom_properties, // Use the filtered HashMap
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
let encoded_url = encode(¤t_url);
|
||||||
let response = gloo_net::http::Request::post("/api/items")
|
let api_url = format!("/api/urls/{}/items", encoded_url);
|
||||||
|
|
||||||
|
let response = gloo_net::http::Request::post(&api_url)
|
||||||
.json(&item_to_send)
|
.json(&item_to_send)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.send()
|
.send()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
log!("[FRONTEND] Save response status: {:?}", response.as_ref().map(|r| r.status()));
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
|
@ -172,51 +210,6 @@ pub fn ItemsList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//function to load items from database
|
|
||||||
async fn load_items_from_db() -> Result<Vec<Item>, 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<DbItem>
|
|
||||||
log!("Loading items from DB...");
|
|
||||||
let db_items = response
|
|
||||||
.json::<Vec<DbItem>>()
|
|
||||||
.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<String, String> =
|
|
||||||
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()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to remove an item
|
// Function to remove an item
|
||||||
let remove_item = move |index: usize| {
|
let remove_item = move |index: usize| {
|
||||||
let item_id = items.get()[index].id.clone();
|
let item_id = items.get()[index].id.clone();
|
||||||
|
@ -434,7 +427,10 @@ pub fn ItemsList(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a new custom property
|
// Add a new custom property
|
||||||
let add_property = move |property: String| {
|
let add_property = {
|
||||||
|
let current_url = Rc::clone(¤t_url);
|
||||||
|
let set_items = set_items.clone();
|
||||||
|
Arc::new(move |property: String| {
|
||||||
// Normalize the property ID
|
// Normalize the property ID
|
||||||
let normalized_property = property.replace("http://www.wikidata.org/prop/", "");
|
let normalized_property = property.replace("http://www.wikidata.org/prop/", "");
|
||||||
|
|
||||||
|
@ -454,8 +450,11 @@ pub fn ItemsList(
|
||||||
|
|
||||||
// Save the updated item to the database
|
// Save the updated item to the database
|
||||||
let item_clone = item.clone();
|
let item_clone = item.clone();
|
||||||
spawn_local(async move {
|
spawn_local({
|
||||||
save_item_to_db(item_clone, selected_properties).await;
|
let current_url = Rc::clone(¤t_url);
|
||||||
|
async move {
|
||||||
|
save_item_to_db(item_clone, selected_properties, current_url.to_string()).await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -504,11 +503,16 @@ pub fn ItemsList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
})};
|
||||||
|
|
||||||
// Update item fields
|
// Update item fields
|
||||||
let update_item = move |index: usize, field: &str, value: String| {
|
let update_item = {
|
||||||
set_items.update(|items| {
|
let set_items = set_items.clone();
|
||||||
|
let current_url = Rc::clone(¤t_url);
|
||||||
|
Arc::new(move |index: usize, field: &str, value: String| {
|
||||||
|
let set_items = set_items.clone();
|
||||||
|
let current_url = Rc::clone(¤t_url);
|
||||||
|
set_items.update(move|items| {
|
||||||
if let Some(item) = items.get_mut(index) {
|
if let Some(item) = items.get_mut(index) {
|
||||||
match field {
|
match field {
|
||||||
"name" => {
|
"name" => {
|
||||||
|
@ -519,8 +523,6 @@ pub fn ItemsList(
|
||||||
if !value.is_empty() {
|
if !value.is_empty() {
|
||||||
if let Some(wikidata_id) = &item.wikidata_id {
|
if let Some(wikidata_id) = &item.wikidata_id {
|
||||||
let wikidata_id = wikidata_id.clone();
|
let wikidata_id = wikidata_id.clone();
|
||||||
let set_fetched_properties = set_fetched_properties.clone();
|
|
||||||
let set_property_labels = set_property_labels.clone();
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let properties = fetch_item_properties(&wikidata_id).await;
|
let properties = fetch_item_properties(&wikidata_id).await;
|
||||||
log!("Fetched properties for index {}: {:?}", index, properties);
|
log!("Fetched properties for index {}: {:?}", index, properties);
|
||||||
|
@ -539,8 +541,11 @@ pub fn ItemsList(
|
||||||
|
|
||||||
// Save the updated item to the database
|
// Save the updated item to the database
|
||||||
let item_clone = item.clone();
|
let item_clone = item.clone();
|
||||||
spawn_local(async move {
|
spawn_local({
|
||||||
save_item_to_db(item_clone, selected_properties).await;
|
let current_url = Rc::clone(¤t_url);
|
||||||
|
async move {
|
||||||
|
save_item_to_db(item_clone, selected_properties, current_url.to_string()).await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Automatically add a new row when editing the last row
|
// Automatically add a new row when editing the last row
|
||||||
|
@ -556,13 +561,16 @@ pub fn ItemsList(
|
||||||
items.push(new_item.clone());
|
items.push(new_item.clone());
|
||||||
|
|
||||||
// Save the new item to the database
|
// Save the new item to the database
|
||||||
spawn_local(async move {
|
spawn_local({
|
||||||
save_item_to_db(new_item, selected_properties).await;
|
let current_url = Rc::clone(¤t_url);
|
||||||
|
async move {
|
||||||
|
save_item_to_db(new_item, selected_properties, current_url.to_string()).await;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
log!("Items updated: {:?}", items);
|
log!("Items updated: {:?}", items);
|
||||||
});
|
});
|
||||||
};
|
})};
|
||||||
|
|
||||||
// List of properties to display as rows
|
// List of properties to display as rows
|
||||||
let properties = vec!["Name", "Description"];
|
let properties = vec!["Name", "Description"];
|
||||||
|
@ -586,11 +594,13 @@ pub fn ItemsList(
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{properties.into_iter().map(|property| {
|
{properties.into_iter().map(|property| {
|
||||||
|
let update_item_cloned = Arc::clone(&update_item);
|
||||||
log!("Rendering property: {}", property);
|
log!("Rendering property: {}", property);
|
||||||
view! {
|
view! {
|
||||||
<tr>
|
<tr>
|
||||||
<td>{ property }</td>
|
<td>{ property }</td>
|
||||||
{move || items.get().iter().enumerate().map(|(index, item)| {
|
{move || items.get().iter().enumerate().map(|(index, item)| {
|
||||||
|
let update_item_clone = Arc::clone(&update_item_cloned);
|
||||||
view! {
|
view! {
|
||||||
<td>
|
<td>
|
||||||
{match property {
|
{match property {
|
||||||
|
@ -599,7 +609,7 @@ pub fn ItemsList(
|
||||||
<EditableCell
|
<EditableCell
|
||||||
value=item.name.clone()
|
value=item.name.clone()
|
||||||
on_input=move |value| {
|
on_input=move |value| {
|
||||||
update_item(index, "name", value.clone());
|
update_item_clone(index, "name", value.clone());
|
||||||
fetch_wikidata_suggestions(format!("name-{}", index), value);
|
fetch_wikidata_suggestions(format!("name-{}", index), value);
|
||||||
}
|
}
|
||||||
key=Arc::new(format!("name-{}", index))
|
key=Arc::new(format!("name-{}", index))
|
||||||
|
@ -661,8 +671,6 @@ pub fn ItemsList(
|
||||||
|
|
||||||
// Fetch additional properties from Wikidata
|
// Fetch additional properties from Wikidata
|
||||||
let wikidata_id = id.clone();
|
let wikidata_id = id.clone();
|
||||||
let set_fetched_properties = set_fetched_properties.clone();
|
|
||||||
let set_property_labels = set_property_labels.clone();
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let properties = fetch_item_properties(&wikidata_id).await;
|
let properties = fetch_item_properties(&wikidata_id).await;
|
||||||
// log!("Fetched properties for Wikidata ID {}: {:?}", wikidata_id, properties);
|
// log!("Fetched properties for Wikidata ID {}: {:?}", wikidata_id, properties);
|
||||||
|
@ -702,7 +710,7 @@ pub fn ItemsList(
|
||||||
"Description" => view! {
|
"Description" => view! {
|
||||||
<EditableCell
|
<EditableCell
|
||||||
value=item.description.clone()
|
value=item.description.clone()
|
||||||
on_input=move |value| update_item(index, "description", value)
|
on_input=move |value| update_item_clone(index, "description", value)
|
||||||
key=Arc::new(format!("description-{}", index))
|
key=Arc::new(format!("description-{}", index))
|
||||||
focused_cell=focused_cell
|
focused_cell=focused_cell
|
||||||
set_focused_cell=set_focused_cell.clone()
|
set_focused_cell=set_focused_cell.clone()
|
||||||
|
@ -726,9 +734,14 @@ pub fn ItemsList(
|
||||||
}
|
}
|
||||||
}).collect::<Vec<_>>()}
|
}).collect::<Vec<_>>()}
|
||||||
// Dynamically adding custom properties as columns
|
// Dynamically adding custom properties as columns
|
||||||
{move || {
|
{{
|
||||||
|
let update_item_outer = Arc::clone(&update_item);
|
||||||
|
|
||||||
|
move || {
|
||||||
|
let update_item = Arc::clone(&update_item_outer);
|
||||||
let custom_props = custom_properties.get().clone();
|
let custom_props = custom_properties.get().clone();
|
||||||
custom_props.into_iter().map(move |property| {
|
custom_props.into_iter().map(move |property| {
|
||||||
|
let update_item_inner = Arc::clone(&update_item);
|
||||||
let normalized_property = property.replace("http://www.wikidata.org/prop/", "");
|
let normalized_property = property.replace("http://www.wikidata.org/prop/", "");
|
||||||
let property_label = property_labels.get().get(&normalized_property).cloned().unwrap_or_else(|| normalized_property.clone());
|
let property_label = property_labels.get().get(&normalized_property).cloned().unwrap_or_else(|| normalized_property.clone());
|
||||||
log!("Rendering property: {} -> {}", normalized_property, property_label);
|
log!("Rendering property: {} -> {}", normalized_property, property_label);
|
||||||
|
@ -754,14 +767,16 @@ pub fn ItemsList(
|
||||||
}>{ "Delete" }</button>
|
}>{ "Delete" }</button>
|
||||||
</td>
|
</td>
|
||||||
{move || {
|
{move || {
|
||||||
|
let update_item_cell = Arc::clone(&update_item_inner);
|
||||||
let property_clone_for_cells = normalized_property.clone();
|
let property_clone_for_cells = normalized_property.clone();
|
||||||
items.get().iter().enumerate().map(move |(index, item)| {
|
items.get().iter().enumerate().map(move |(index, item)| {
|
||||||
let property_clone_for_closure = property_clone_for_cells.clone();
|
let update_item_cell = Arc::clone(&update_item_cell);
|
||||||
|
let property_clone_for_closure = property_clone_for_cells.clone();
|
||||||
view! {
|
view! {
|
||||||
<td>
|
<td>
|
||||||
<EditableCell
|
<EditableCell
|
||||||
value=item.custom_properties.get(&property_clone_for_closure).cloned().unwrap_or_default()
|
value=item.custom_properties.get(&property_clone_for_closure).cloned().unwrap_or_default()
|
||||||
on_input=move |value| update_item(index, &property_clone_for_closure, value)
|
on_input=move |value| update_item_cell(index, &property_clone_for_closure, value)
|
||||||
key=Arc::new(format!("custom-{}-{}", property_clone_for_cells, index))
|
key=Arc::new(format!("custom-{}-{}", property_clone_for_cells, index))
|
||||||
focused_cell=focused_cell
|
focused_cell=focused_cell
|
||||||
set_focused_cell=set_focused_cell.clone()
|
set_focused_cell=set_focused_cell.clone()
|
||||||
|
@ -777,7 +792,7 @@ pub fn ItemsList(
|
||||||
}
|
}
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
}).collect::<Vec<_>>()
|
}).collect::<Vec<_>>()}
|
||||||
}}
|
}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
272
src/db.rs
272
src/db.rs
|
@ -5,6 +5,9 @@ mod db_impl {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use leptos::logging;
|
use leptos::logging;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use crate::models::item::Item;
|
||||||
|
use leptos::logging::log;
|
||||||
|
|
||||||
// Define a struct to represent a database connection
|
// Define a struct to represent a database connection
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -25,41 +28,70 @@ mod db_impl {
|
||||||
// Create the database schema
|
// Create the database schema
|
||||||
pub async fn create_schema(&self) -> Result<(), Error> {
|
pub async fn create_schema(&self) -> Result<(), Error> {
|
||||||
let conn = self.conn.lock().await;
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
// 1. Properties table
|
||||||
|
conn.execute_batch(
|
||||||
|
"CREATE TABLE IF NOT EXISTS properties (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
global_usage_count INTEGER DEFAULT 0
|
||||||
|
);"
|
||||||
|
).map_err(|e| {
|
||||||
|
eprintln!("Failed creating properties table: {}", e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 2. URLs table
|
||||||
|
conn.execute_batch(
|
||||||
|
"CREATE TABLE IF NOT EXISTS urls (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
url TEXT NOT NULL UNIQUE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);",
|
||||||
|
).map_err(|e| {
|
||||||
|
eprintln!("Failed creating urls table: {}", e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 3. Items table
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
"CREATE TABLE IF NOT EXISTS items (
|
"CREATE TABLE IF NOT EXISTS items (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
|
url_id INTEGER NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
wikidata_id TEXT,
|
wikidata_id TEXT,
|
||||||
custom_properties TEXT
|
FOREIGN KEY (url_id) REFERENCES urls(id) ON DELETE CASCADE
|
||||||
);",
|
);",
|
||||||
)?;
|
).map_err(|e| {
|
||||||
logging::log!("Database schema created or verified");
|
eprintln!("Failed creating items table: {}", e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 4. Junction table for custom properties
|
||||||
|
conn.execute_batch(
|
||||||
|
"CREATE TABLE IF NOT EXISTS item_properties (
|
||||||
|
item_id TEXT NOT NULL,
|
||||||
|
property_id INTEGER NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (item_id, property_id),
|
||||||
|
FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (property_id) REFERENCES properties(id) ON DELETE CASCADE
|
||||||
|
);"
|
||||||
|
).map_err(|e| {
|
||||||
|
eprintln!("Failed creating item_properties table: {}", e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert a new item into the database
|
// Insert a new URL into the database
|
||||||
pub async fn insert_item(&self, item: &DbItem) -> Result<(), Error> {
|
pub async fn insert_url(&self, url: &str) -> Result<i64, Error> {
|
||||||
let conn = self.conn.lock().await;
|
let conn = self.conn.lock().await;
|
||||||
let wikidata_id = item.wikidata_id.as_ref().map(|s| s.as_str()).unwrap_or("");
|
let mut stmt = conn.prepare("INSERT INTO urls (url) VALUES (?)")?;
|
||||||
conn.execute(
|
let url_id = stmt.insert(&[url])?;
|
||||||
"INSERT INTO items (id, name, description, wikidata_id, custom_properties)
|
logging::log!("URL inserted: {}", url);
|
||||||
VALUES (?, ?, ?, ?, ?)
|
Ok(url_id)
|
||||||
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> {
|
pub async fn delete_item(&self, item_id: &str) -> Result<(), Error> {
|
||||||
|
@ -87,7 +119,6 @@ mod db_impl {
|
||||||
name: row.get(1)?,
|
name: row.get(1)?,
|
||||||
description: row.get(2)?,
|
description: row.get(2)?,
|
||||||
wikidata_id: row.get(3)?,
|
wikidata_id: row.get(3)?,
|
||||||
custom_properties: row.get(4)?,
|
|
||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
|
@ -97,6 +128,172 @@ mod db_impl {
|
||||||
logging::log!("Fetched {} items from the database", result.len()); // Log with Leptos
|
logging::log!("Fetched {} items from the database", result.len()); // Log with Leptos
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retrieve all items from the database for a specific URL
|
||||||
|
pub async fn get_items_by_url(&self, url: &str) -> Result<Vec<Item>, Error> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
let url_id: i64 = conn.query_row("SELECT id FROM urls WHERE url = ?", &[url], |row| row.get(0))?;
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT i.id, i.name, i.description, i.wikidata_id,
|
||||||
|
p.name AS prop_name, ip.value
|
||||||
|
FROM items i
|
||||||
|
LEFT JOIN item_properties ip ON i.id = ip.item_id
|
||||||
|
LEFT JOIN properties p ON ip.property_id = p.id
|
||||||
|
WHERE i.url_id = ?"
|
||||||
|
)?;
|
||||||
|
let mut items: HashMap<String, Item> = HashMap::new();
|
||||||
|
|
||||||
|
let rows = stmt.query_map([url_id], |row| {
|
||||||
|
Ok((
|
||||||
|
row.get::<_, String>(0)?, // id
|
||||||
|
row.get::<_, String>(1)?, // name
|
||||||
|
row.get::<_, String>(2)?, // description
|
||||||
|
row.get::<_, Option<String>>(3)?, // wikidata_id
|
||||||
|
row.get::<_, Option<String>>(4)?, // prop_name
|
||||||
|
row.get::<_, Option<String>>(5)?, // value
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for row in rows {
|
||||||
|
let (id, name, desc, wd_id, prop, val) = row?;
|
||||||
|
let item = items.entry(id.clone()).or_insert(Item {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
description: desc,
|
||||||
|
wikidata_id: wd_id,
|
||||||
|
custom_properties: HashMap::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if let (Some(p), Some(v)) = (prop, val) {
|
||||||
|
item.custom_properties.insert(p, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(items.into_values().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_url_id(&self, url: &str) -> Result<Option<i64>, Error> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
conn.query_row(
|
||||||
|
"SELECT id FROM urls WHERE url = ?",
|
||||||
|
&[url],
|
||||||
|
|row| row.get(0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_or_create_property(&self, prop: &str) -> Result<i64, Error> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
// Check existing
|
||||||
|
let exists: Result<i64, _> = conn.query_row(
|
||||||
|
"SELECT id FROM properties WHERE name = ?",
|
||||||
|
&[prop],
|
||||||
|
|row| row.get(0)
|
||||||
|
);
|
||||||
|
|
||||||
|
match exists {
|
||||||
|
Ok(id) => Ok(id),
|
||||||
|
Err(_) => {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO properties (name) VALUES (?)",
|
||||||
|
&[prop],
|
||||||
|
)?;
|
||||||
|
Ok(conn.last_insert_rowid())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert a new item into the database for a specific URL
|
||||||
|
pub async fn insert_item_by_url(
|
||||||
|
&self,
|
||||||
|
url: &str,
|
||||||
|
item: &Item
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
// Log before DB operations
|
||||||
|
log!("[DATABASE] Inserting item - ID: {}, Name: '{}'", item.id, item.name);
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
// Get or create URL record
|
||||||
|
let url_id = match self.get_url_id(url).await {
|
||||||
|
Ok(Some(id)) => id,
|
||||||
|
_ => self.insert_url(url).await?,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log final SQL parameters
|
||||||
|
log!("[DATABASE] SQL params - ID: {}, URL ID: {}, Name: '{}'",
|
||||||
|
item.id, url_id, item.name);
|
||||||
|
|
||||||
|
// Insert item with URL relationship
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO items (id, url_id, name, description, wikidata_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?)",
|
||||||
|
&[&item.id, &url_id.to_string(), &item.name,
|
||||||
|
&item.description, &item.wikidata_id.as_ref().unwrap_or(&String::new())],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Handle properties through junction table
|
||||||
|
for (prop, value) in &item.custom_properties {
|
||||||
|
let prop_id = self.get_or_create_property(&prop).await?;
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO item_properties (item_id, property_id, value)
|
||||||
|
VALUES (?, ?, ?)",
|
||||||
|
&[&item.id, &prop_id.to_string(), &value],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
log!("[DATABASE] Successfully inserted item ID: {}", item.id);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete an item from the database for a specific URL
|
||||||
|
pub async fn delete_item_by_url(&self, url: &str, item_id: &str) -> Result<(), Error> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
let url_id: i64 = conn.query_row("SELECT id FROM urls WHERE url = ?", &[url], |row| row.get(0))?;
|
||||||
|
conn.execute("DELETE FROM items WHERE id = ? AND url_id = ?", &[item_id, &url_id.to_string()])?;
|
||||||
|
logging::log!("Item deleted from the database for URL: {}", url);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a property from the database for a specific URL
|
||||||
|
pub async fn delete_property_by_url(&self, url: &str, property: &str) -> Result<(), Error> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
let url_id: i64 = conn.query_row("SELECT id FROM urls WHERE url = ?", &[url], |row| row.get(0))?;
|
||||||
|
|
||||||
|
// Delete from junction table instead of JSON
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM item_properties
|
||||||
|
WHERE property_id IN (
|
||||||
|
SELECT id FROM properties WHERE name = ?
|
||||||
|
) AND item_id IN (
|
||||||
|
SELECT id FROM items WHERE url_id = ?
|
||||||
|
)",
|
||||||
|
&[property, &url_id.to_string()],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
logging::log!("Property deleted from the database for URL: {}", url);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
// function to log database state
|
||||||
|
pub async fn debug_dump(&self) -> Result<(), Error> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
log!("[DATABASE DEBUG] URLs:");
|
||||||
|
let mut stmt = conn.prepare("SELECT id, url FROM urls")?;
|
||||||
|
let urls = stmt.query_map([], |row| {
|
||||||
|
Ok(format!("ID: {}, URL: {}", row.get::<_, i64>(0)?, row.get::<_, String>(1)?))
|
||||||
|
})?;
|
||||||
|
for url in urls {
|
||||||
|
log!("[DATABASE DEBUG] {}", url?);
|
||||||
|
}
|
||||||
|
|
||||||
|
log!("[DATABASE DEBUG] Items:");
|
||||||
|
let mut stmt = conn.prepare("SELECT id, name FROM items")?;
|
||||||
|
let items = stmt.query_map([], |row| {
|
||||||
|
Ok(format!("ID: {}, Name: '{}'", row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||||
|
})?;
|
||||||
|
for item in items {
|
||||||
|
log!("[DATABASE DEBUG] {}", item?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define a struct to represent an item in the database
|
// Define a struct to represent an item in the database
|
||||||
|
@ -106,31 +303,8 @@ mod db_impl {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub wikidata_id: Option<String>,
|
pub wikidata_id: Option<String>,
|
||||||
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<String>,
|
|
||||||
pub custom_properties: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<DbItem> 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")]
|
#[cfg(feature = "ssr")]
|
||||||
pub use db_impl::{Database, DbItem, ItemResponse};
|
pub use db_impl::{Database, DbItem};
|
76
src/main.rs
76
src/main.rs
|
@ -1,4 +1,11 @@
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use compareware::db::Database;
|
||||||
|
use compareware::api::{ItemRequest,create_item, get_items, delete_item_by_url};
|
||||||
|
use compareware::models::item::Item;
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
use actix_files::Files;
|
use actix_files::Files;
|
||||||
|
@ -6,19 +13,21 @@ async fn main() -> std::io::Result<()> {
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_actix::{generate_route_list, LeptosRoutes};
|
use leptos_actix::{generate_route_list, LeptosRoutes};
|
||||||
use compareware::app::*;
|
use compareware::app::*;
|
||||||
use compareware::db::{Database, DbItem};
|
use compareware::db::Database;
|
||||||
use compareware::api::{get_items, create_item, delete_item, delete_property}; // Import API handlers
|
use compareware::api::{get_items, create_item, delete_item, delete_property}; // Import API handlers
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
// Load configuration
|
|
||||||
let conf = get_configuration(None).await.unwrap();
|
|
||||||
let addr = conf.leptos_options.site_addr;
|
|
||||||
|
|
||||||
// Initialize the database
|
// Initialize the database
|
||||||
let db = Database::new("compareware.db").unwrap();
|
let db = Database::new("compareware.db").unwrap();
|
||||||
db.create_schema().await.unwrap(); // Ensure the schema is created
|
db.create_schema().await.unwrap(); // Ensure the schema is created
|
||||||
let db = Arc::new(Mutex::new(db)); // Wrap the database in an Arc<Mutex<T>> for shared state
|
let db = Arc::new(Mutex::new(db)); // Wrap the database in an Arc<Mutex<T>> for shared state
|
||||||
|
println!("Schema created successfully!");
|
||||||
|
|
||||||
|
// Load configuration
|
||||||
|
let conf = get_configuration(None).await.unwrap();
|
||||||
|
let addr = conf.leptos_options.site_addr;
|
||||||
|
|
||||||
|
|
||||||
// Generate the list of routes in your Leptos App
|
// Generate the list of routes in your Leptos App
|
||||||
let routes = generate_route_list(App);
|
let routes = generate_route_list(App);
|
||||||
|
@ -32,12 +41,16 @@ async fn main() -> std::io::Result<()> {
|
||||||
|
|
||||||
|
|
||||||
App::new()
|
App::new()
|
||||||
|
.app_data(web::Data::new(db.clone()))
|
||||||
// Register custom API routes BEFORE Leptos server functions
|
// Register custom API routes BEFORE Leptos server functions
|
||||||
.service(
|
.service(
|
||||||
web::scope("/api")
|
web::scope("/api")
|
||||||
.route("/items", web::get().to(get_items)) // GET /api/items
|
.service(
|
||||||
.route("/items", web::post().to(create_item)) // POST /api/items
|
web::scope("/urls/{url}")
|
||||||
.route("/items/{id}", web::delete().to(delete_item)) // DELETE /api/items/{id}
|
.route("/items", web::get().to(get_items_handler)) // GET items by URL
|
||||||
|
.route("/items", web::post().to(create_item_handler)) // Create item for URL
|
||||||
|
.route("/items/{item_id}", web::delete().to(delete_item_handler)) // Delete item
|
||||||
|
)
|
||||||
.route("/properties/{property}", web::delete().to(delete_property)), // DELETE /api/properties/{property}
|
.route("/properties/{property}", web::delete().to(delete_property)), // DELETE /api/properties/{property}
|
||||||
)
|
)
|
||||||
// Register server functions
|
// Register server functions
|
||||||
|
@ -55,12 +68,57 @@ async fn main() -> std::io::Result<()> {
|
||||||
//.wrap(middleware::Compress::default())
|
//.wrap(middleware::Compress::default())
|
||||||
// Pass the database as shared state
|
// Pass the database as shared state
|
||||||
.app_data(web::Data::new(db))
|
.app_data(web::Data::new(db))
|
||||||
|
// Register URL routing
|
||||||
|
.service(web::resource("/").route(web::get().to(index)))
|
||||||
|
.service(web::resource("/{url}").route(web::get().to(url_handler)))
|
||||||
})
|
})
|
||||||
.bind(&addr)?
|
.bind(&addr)?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler to get items for a specific URL
|
||||||
|
async fn get_items_handler(
|
||||||
|
db: web::Data<Arc<Mutex<Database>>>,
|
||||||
|
url: web::Path<String>,
|
||||||
|
) -> impl Responder {
|
||||||
|
get_items(db, web::Query(url.into_inner())).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler to create an item for a specific URL
|
||||||
|
async fn create_item_handler(
|
||||||
|
db: web::Data<Arc<Mutex<Database>>>,
|
||||||
|
url: web::Path<String>,
|
||||||
|
item: web::Json<Item>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let request = ItemRequest {
|
||||||
|
url: url.into_inner(),
|
||||||
|
item: item.into_inner()
|
||||||
|
};
|
||||||
|
create_item(db, web::Json(request)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler to delete an item for a specific URL
|
||||||
|
async fn delete_item_handler(
|
||||||
|
db: web::Data<Arc<Mutex<Database>>>,
|
||||||
|
path: web::Path<(String, String)>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let (url, item_id) = path.into_inner();
|
||||||
|
delete_item_by_url(db, web::Path::from(url), web::Path::from(item_id)).await
|
||||||
|
}
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
// Define the index handler
|
||||||
|
async fn index() -> HttpResponse {
|
||||||
|
HttpResponse::Ok().body("Welcome to CompareWare!")
|
||||||
|
}
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
|
// Define the URL handler
|
||||||
|
async fn url_handler(url: web::Path<String>) -> HttpResponse {
|
||||||
|
let url = url.into_inner();
|
||||||
|
// TO DO: Implement URL-based content storage and editing functionality
|
||||||
|
HttpResponse::Ok().body(format!("You are viewing the content at {}", url))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
#[actix_web::get("favicon.ico")]
|
#[actix_web::get("favicon.ico")]
|
||||||
async fn favicon(
|
async fn favicon(
|
||||||
|
|
|
@ -7,13 +7,6 @@ pub struct Item {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
// pub reviews: Vec<ReviewWithRating>,
|
|
||||||
pub wikidata_id: Option<String>,
|
pub wikidata_id: Option<String>,
|
||||||
pub custom_properties: HashMap<String, String>,
|
pub custom_properties: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
||||||
pub struct ReviewWithRating {
|
|
||||||
pub content: String,
|
|
||||||
pub rating: u8, // Ratings from 1 to 5
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue