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")]
|
||||
use actix_web::{web, HttpResponse};
|
||||
#[cfg(feature = "ssr")]
|
||||
use crate::db::{Database, DbItem};
|
||||
use crate::db::Database;
|
||||
#[cfg(feature = "ssr")]
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "ssr")]
|
||||
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")]
|
||||
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;
|
||||
match db.get_items().await {
|
||||
match db.get_items_by_url(&url).await {
|
||||
Ok(items) => HttpResponse::Ok().json(items),
|
||||
Err(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")]
|
||||
pub async fn create_item(
|
||||
db: web::Data<Arc<Mutex<Database>>>,
|
||||
item: web::Json<DbItem>,
|
||||
request: web::Json<ItemRequest>,
|
||||
) -> 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")
|
||||
let url = request.url.clone();
|
||||
let item = request.item.clone();
|
||||
let item_id = request.item.id.clone();
|
||||
// request logging
|
||||
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")]
|
||||
pub async fn delete_item(
|
||||
db: web::Data<Arc<Mutex<Database>>>,
|
||||
url: web::Query<String>,
|
||||
item_id: web::Path<String>,
|
||||
) -> HttpResponse {
|
||||
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"),
|
||||
Err(err) => {
|
||||
leptos::logging::error!("Failed to delete item: {:?}", err);
|
||||
|
@ -52,10 +85,11 @@ pub async fn delete_item(
|
|||
#[cfg(feature = "ssr")]
|
||||
pub async fn delete_property(
|
||||
db: web::Data<Arc<Mutex<Database>>>,
|
||||
url: web::Query<String>,
|
||||
property: web::Path<String>,
|
||||
) -> HttpResponse {
|
||||
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"),
|
||||
Err(err) => {
|
||||
leptos::logging::error!("Failed to delete property: {:?}", err);
|
||||
|
@ -63,3 +97,51 @@ pub async fn 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")
|
||||
}
|
||||
}
|
||||
}
|
27
src/app.rs
27
src/app.rs
|
@ -1,6 +1,8 @@
|
|||
use leptos::*;
|
||||
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::nostr::NostrClient;
|
||||
use tokio::sync::mpsc;
|
||||
|
@ -26,7 +28,26 @@ pub fn App() -> impl IntoView {
|
|||
}
|
||||
}
|
||||
});
|
||||
view! {
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/:url?" view=move || {
|
||||
let params = use_params_map();
|
||||
let url = move || params.with(|params| params.get("url").cloned().unwrap_or_default());
|
||||
|
||||
// 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" />
|
||||
|
@ -35,4 +56,8 @@ pub fn App() -> impl IntoView {
|
|||
<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::sync::Arc;
|
||||
use wasm_bindgen::JsCast;
|
||||
use std::rc::Rc;
|
||||
use urlencoding::encode;
|
||||
use gloo_net::http::Request;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
struct WikidataSuggestion {
|
||||
id: String,
|
||||
|
@ -19,14 +17,28 @@ struct WikidataSuggestion {
|
|||
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)]
|
||||
struct DbItem {
|
||||
id: String,
|
||||
name: String,
|
||||
description: String,
|
||||
wikidata_id: Option<String>,
|
||||
custom_properties: String,
|
||||
if response.status() == 200 {
|
||||
// Deserialize into Vec<Item>
|
||||
log!("Loading items from DB...");
|
||||
let items = response
|
||||
.json::<Vec<Item>>()
|
||||
.await
|
||||
.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]
|
||||
|
@ -52,8 +64,29 @@ pub fn ItemsList(
|
|||
// Signal to store the fetched property labels
|
||||
let (property_labels, set_property_labels) = create_signal(HashMap::<String, String>::new());
|
||||
|
||||
spawn_local(async move {
|
||||
match load_items_from_db().await {
|
||||
#[cfg(feature = "ssr")]
|
||||
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) => {
|
||||
// Set the loaded items
|
||||
if loaded_items.is_empty() {
|
||||
|
@ -107,7 +140,7 @@ pub fn ItemsList(
|
|||
log!("Error loading items: {}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}});
|
||||
|
||||
|
||||
// Ensure there's an initial empty row
|
||||
|
@ -116,50 +149,55 @@ pub fn ItemsList(
|
|||
id: Uuid::new_v4().to_string(),
|
||||
name: String::new(),
|
||||
description: String::new(),
|
||||
// reviews: vec![],
|
||||
wikidata_id: None,
|
||||
custom_properties: HashMap::new(),
|
||||
}]);
|
||||
}
|
||||
|
||||
// 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`
|
||||
let custom_properties: HashMap<String, String> = (move || {
|
||||
let selected_props = selected_properties.get(); // Access the signal inside a reactive closure
|
||||
item.custom_properties
|
||||
custom_props
|
||||
.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<String>,
|
||||
custom_properties: String, // JSON-encoded string
|
||||
struct ItemRequest {
|
||||
url: String,
|
||||
item: Item,
|
||||
}
|
||||
|
||||
let item_to_send = ItemToSend {
|
||||
log!("[FRONTEND] Saving item - ID: {}, Name: '{}', Properties: {:?}",
|
||||
item.id, item.name, item.custom_properties);
|
||||
|
||||
let item_to_send = ItemRequest {
|
||||
url: current_url.to_string(),
|
||||
item: Item {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
wikidata_id: item.wikidata_id,
|
||||
custom_properties, // Use the serialized string
|
||||
custom_properties, // Use the filtered HashMap
|
||||
},
|
||||
};
|
||||
let encoded_url = encode(¤t_url);
|
||||
let api_url = format!("/api/urls/{}/items", encoded_url);
|
||||
|
||||
let response = gloo_net::http::Request::post("/api/items")
|
||||
let response = gloo_net::http::Request::post(&api_url)
|
||||
.json(&item_to_send)
|
||||
.unwrap()
|
||||
.send()
|
||||
.await;
|
||||
|
||||
log!("[FRONTEND] Save response status: {:?}", response.as_ref().map(|r| r.status()));
|
||||
|
||||
match response {
|
||||
Ok(resp) => {
|
||||
if resp.status() == 200 {
|
||||
|
@ -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
|
||||
let remove_item = move |index: usize| {
|
||||
let item_id = items.get()[index].id.clone();
|
||||
|
@ -434,7 +427,10 @@ pub fn ItemsList(
|
|||
}
|
||||
|
||||
// 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
|
||||
let normalized_property = property.replace("http://www.wikidata.org/prop/", "");
|
||||
|
||||
|
@ -454,8 +450,11 @@ pub fn ItemsList(
|
|||
|
||||
// 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;
|
||||
spawn_local({
|
||||
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
|
||||
let update_item = move |index: usize, field: &str, value: String| {
|
||||
set_items.update(|items| {
|
||||
let update_item = {
|
||||
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) {
|
||||
match field {
|
||||
"name" => {
|
||||
|
@ -519,8 +523,6 @@ pub fn ItemsList(
|
|||
if !value.is_empty() {
|
||||
if let Some(wikidata_id) = &item.wikidata_id {
|
||||
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 {
|
||||
let properties = fetch_item_properties(&wikidata_id).await;
|
||||
log!("Fetched properties for index {}: {:?}", index, properties);
|
||||
|
@ -539,8 +541,11 @@ pub fn ItemsList(
|
|||
|
||||
// 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;
|
||||
spawn_local({
|
||||
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
|
||||
|
@ -556,13 +561,16 @@ pub fn ItemsList(
|
|||
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;
|
||||
spawn_local({
|
||||
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);
|
||||
});
|
||||
};
|
||||
})};
|
||||
|
||||
// List of properties to display as rows
|
||||
let properties = vec!["Name", "Description"];
|
||||
|
@ -586,11 +594,13 @@ pub fn ItemsList(
|
|||
</thead>
|
||||
<tbody>
|
||||
{properties.into_iter().map(|property| {
|
||||
let update_item_cloned = Arc::clone(&update_item);
|
||||
log!("Rendering property: {}", property);
|
||||
view! {
|
||||
<tr>
|
||||
<td>{ property }</td>
|
||||
{move || items.get().iter().enumerate().map(|(index, item)| {
|
||||
let update_item_clone = Arc::clone(&update_item_cloned);
|
||||
view! {
|
||||
<td>
|
||||
{match property {
|
||||
|
@ -599,7 +609,7 @@ pub fn ItemsList(
|
|||
<EditableCell
|
||||
value=item.name.clone()
|
||||
on_input=move |value| {
|
||||
update_item(index, "name", value.clone());
|
||||
update_item_clone(index, "name", value.clone());
|
||||
fetch_wikidata_suggestions(format!("name-{}", index), value);
|
||||
}
|
||||
key=Arc::new(format!("name-{}", index))
|
||||
|
@ -661,8 +671,6 @@ pub fn ItemsList(
|
|||
|
||||
// Fetch additional properties from Wikidata
|
||||
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 {
|
||||
let properties = fetch_item_properties(&wikidata_id).await;
|
||||
// log!("Fetched properties for Wikidata ID {}: {:?}", wikidata_id, properties);
|
||||
|
@ -702,7 +710,7 @@ pub fn ItemsList(
|
|||
"Description" => view! {
|
||||
<EditableCell
|
||||
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))
|
||||
focused_cell=focused_cell
|
||||
set_focused_cell=set_focused_cell.clone()
|
||||
|
@ -726,9 +734,14 @@ pub fn ItemsList(
|
|||
}
|
||||
}).collect::<Vec<_>>()}
|
||||
// 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();
|
||||
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 property_label = property_labels.get().get(&normalized_property).cloned().unwrap_or_else(|| normalized_property.clone());
|
||||
log!("Rendering property: {} -> {}", normalized_property, property_label);
|
||||
|
@ -754,14 +767,16 @@ pub fn ItemsList(
|
|||
}>{ "Delete" }</button>
|
||||
</td>
|
||||
{move || {
|
||||
let update_item_cell = Arc::clone(&update_item_inner);
|
||||
let property_clone_for_cells = normalized_property.clone();
|
||||
items.get().iter().enumerate().map(move |(index, item)| {
|
||||
let update_item_cell = Arc::clone(&update_item_cell);
|
||||
let property_clone_for_closure = property_clone_for_cells.clone();
|
||||
view! {
|
||||
<td>
|
||||
<EditableCell
|
||||
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))
|
||||
focused_cell=focused_cell
|
||||
set_focused_cell=set_focused_cell.clone()
|
||||
|
@ -777,7 +792,7 @@ pub fn ItemsList(
|
|||
}
|
||||
</tr>
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
}).collect::<Vec<_>>()}
|
||||
}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
272
src/db.rs
272
src/db.rs
|
@ -5,6 +5,9 @@ mod db_impl {
|
|||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use leptos::logging;
|
||||
use std::collections::HashMap;
|
||||
use crate::models::item::Item;
|
||||
use leptos::logging::log;
|
||||
|
||||
// Define a struct to represent a database connection
|
||||
#[derive(Debug)]
|
||||
|
@ -25,41 +28,70 @@ mod db_impl {
|
|||
// Create the database schema
|
||||
pub async fn create_schema(&self) -> Result<(), Error> {
|
||||
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(
|
||||
"CREATE TABLE IF NOT EXISTS items (
|
||||
id TEXT PRIMARY KEY,
|
||||
url_id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
wikidata_id TEXT,
|
||||
custom_properties TEXT
|
||||
FOREIGN KEY (url_id) REFERENCES urls(id) ON DELETE CASCADE
|
||||
);",
|
||||
)?;
|
||||
logging::log!("Database schema created or verified");
|
||||
).map_err(|e| {
|
||||
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(())
|
||||
}
|
||||
|
||||
// Insert a new item into the database
|
||||
pub async fn insert_item(&self, item: &DbItem) -> Result<(), Error> {
|
||||
// Insert a new URL into the database
|
||||
pub async fn insert_url(&self, url: &str) -> Result<i64, 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(())
|
||||
let mut stmt = conn.prepare("INSERT INTO urls (url) VALUES (?)")?;
|
||||
let url_id = stmt.insert(&[url])?;
|
||||
logging::log!("URL inserted: {}", url);
|
||||
Ok(url_id)
|
||||
}
|
||||
|
||||
pub async fn delete_item(&self, item_id: &str) -> Result<(), Error> {
|
||||
|
@ -87,7 +119,6 @@ mod db_impl {
|
|||
name: row.get(1)?,
|
||||
description: row.get(2)?,
|
||||
wikidata_id: row.get(3)?,
|
||||
custom_properties: row.get(4)?,
|
||||
})
|
||||
})?;
|
||||
let mut result = Vec::new();
|
||||
|
@ -97,6 +128,172 @@ mod db_impl {
|
|||
logging::log!("Fetched {} items from the database", result.len()); // Log with Leptos
|
||||
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
|
||||
|
@ -106,31 +303,8 @@ mod db_impl {
|
|||
pub name: String,
|
||||
pub description: 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")]
|
||||
pub use db_impl::{Database, DbItem, ItemResponse};
|
||||
pub use db_impl::{Database, DbItem};
|
74
src/main.rs
74
src/main.rs
|
@ -1,4 +1,11 @@
|
|||
#[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]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use actix_files::Files;
|
||||
|
@ -6,19 +13,21 @@ 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::db::Database;
|
||||
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<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
|
||||
let routes = generate_route_list(App);
|
||||
|
@ -32,12 +41,16 @@ async fn main() -> std::io::Result<()> {
|
|||
|
||||
|
||||
App::new()
|
||||
.app_data(web::Data::new(db.clone()))
|
||||
// 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}
|
||||
.service(
|
||||
web::scope("/urls/{url}")
|
||||
.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}
|
||||
)
|
||||
// Register server functions
|
||||
|
@ -55,12 +68,57 @@ async fn main() -> std::io::Result<()> {
|
|||
//.wrap(middleware::Compress::default())
|
||||
// Pass the database as shared state
|
||||
.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)?
|
||||
.run()
|
||||
.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")]
|
||||
#[actix_web::get("favicon.ico")]
|
||||
async fn favicon(
|
||||
|
|
|
@ -7,13 +7,6 @@ pub struct Item {
|
|||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
// pub reviews: Vec<ReviewWithRating>,
|
||||
pub wikidata_id: Option<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