Compare commits
No commits in common. "d77a806fe713ff346b7b7e6584f3afe459dcb879" and "443c7a7e0c6f3856f6fdd417a83568e22f1c6df1" have entirely different histories.
d77a806fe7
...
443c7a7e0c
7 changed files with 252 additions and 975 deletions
32
README.md
32
README.md
|
@ -27,38 +27,6 @@ CompareWare is an open-source platform for comparing tools (software, hardware,
|
||||||
```
|
```
|
||||||
3. Open your browser at [http://localhost:3000](http://localhost:3000)
|
3. Open your browser at [http://localhost:3000](http://localhost:3000)
|
||||||
|
|
||||||
## **Database Schema**
|
|
||||||
### Key Concepts
|
|
||||||
- **PK (Primary Key)**: Unique identifier for table records (🔑)
|
|
||||||
- **FK (Foreign Key)**: Reference linking related tables (➡️)
|
|
||||||
|
|
||||||
### **Tables Overview**
|
|
||||||
|
|
||||||
### Core Tables
|
|
||||||
| Table | Columns (PK/FK) | Description | Example Data |
|
|
||||||
|-------|------------------|-------------|--------------|
|
|
||||||
| **urls** | `id` (PK), `url`, `created_at` | Stores comparison URLs | `1, "/laptops", 2024-03-01` |
|
|
||||||
| **items** | `id` (PK), `url_id` (FK), `name`, `description`, `wikidata_id` | Comparison items | `"item1", 1, "MacBook Pro", "16-inch", "Q214276"` |
|
|
||||||
| **properties** | `id` (PK), `name`, `global_usage_count` | Available properties | `25, "screen_size", 150` |
|
|
||||||
| **item_properties** | `item_id` (PK/FK), `property_id` (PK/FK), `value` | Item-specific values | `"item1", 25, "16 inches"` |
|
|
||||||
| **selected_properties** | `url_id` (PK/FK), `property_id` (PK/FK) | Active properties per URL | `1, 25` |
|
|
||||||
|
|
||||||
### Data Flow
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
User -->|Creates| urls
|
|
||||||
User -->|Adds| items
|
|
||||||
User -->|Defines| properties
|
|
||||||
User -->|Selects| selected_properties
|
|
||||||
User -->|Sets Values| item_properties
|
|
||||||
|
|
||||||
urls -->|url_id| items
|
|
||||||
urls -->|url_id| selected_properties
|
|
||||||
properties -->|property_id| selected_properties
|
|
||||||
items -->|item_id| item_properties
|
|
||||||
properties -->|property_id| item_properties
|
|
||||||
```
|
|
||||||
|
|
||||||
### **Collaboration**
|
### **Collaboration**
|
||||||
We welcome contributions! Here’s how you can help:
|
We welcome contributions! Here’s how you can help:
|
||||||
|
|
||||||
|
|
157
src/api.rs
157
src/api.rs
|
@ -1,42 +1,19 @@
|
||||||
#[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;
|
use crate::db::{Database, DbItem};
|
||||||
#[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")]
|
||||||
use serde::{Deserialize, Serialize};
|
pub async fn get_items(db: web::Data<Arc<Mutex<Database>>>) -> HttpResponse {
|
||||||
#[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 {
|
|
||||||
log!("[SERVER] Received request for URL: {}", url);
|
|
||||||
|
|
||||||
let db = db.lock().await;
|
let db = db.lock().await;
|
||||||
match db.get_items_by_url(&url).await {
|
match db.get_items().await {
|
||||||
Ok(items) => {
|
Ok(items) => HttpResponse::Ok().json(items),
|
||||||
log!("[SERVER] Returning {} items for URL: {}", items.len(), url);
|
|
||||||
HttpResponse::Ok().json(items)
|
|
||||||
},
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log!("[SERVER ERROR] Failed to fetch items for {}: {:?}", url, err);
|
leptos::logging::error!("Failed to fetch items: {:?}", err);
|
||||||
HttpResponse::InternalServerError().body("Failed to fetch items")
|
HttpResponse::InternalServerError().body("Failed to fetch items")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,28 +22,14 @@ pub async fn get_items(
|
||||||
#[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>>>,
|
||||||
request: web::Json<ItemRequest>,
|
item: web::Json<DbItem>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let db = db.lock().await;
|
let db = db.lock().await;
|
||||||
let url = request.url.clone();
|
match db.insert_item(&item.into_inner()).await {
|
||||||
let item = request.item.clone();
|
Ok(_) => HttpResponse::Ok().body("Item inserted"),
|
||||||
let item_id = request.item.id.clone();
|
Err(err) => {
|
||||||
// request logging
|
leptos::logging::error!("Failed to insert item: {:?}", err);
|
||||||
log!("[API] Received item request - URL: {}, Item ID: {}",
|
HttpResponse::InternalServerError().body("Failed to insert item")
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,16 +37,14 @@ 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>>>,
|
||||||
path: web::Path<(String, String)>, // (url, item_id)
|
item_id: web::Path<String>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let (url, item_id) = path.into_inner();
|
|
||||||
log!("[API] Deleting item {} from URL {}", item_id, url);
|
|
||||||
let db = db.lock().await;
|
let db = db.lock().await;
|
||||||
match db.delete_item_by_url(&url, &item_id).await {
|
match db.delete_item(&item_id).await {
|
||||||
Ok(_) => HttpResponse::Ok().finish(),
|
Ok(_) => HttpResponse::Ok().body("Item deleted"),
|
||||||
Err(e) => {
|
Err(err) => {
|
||||||
log!("[API] Delete error: {:?}", e);
|
leptos::logging::error!("Failed to delete item: {:?}", err);
|
||||||
HttpResponse::InternalServerError().body(e.to_string())
|
HttpResponse::InternalServerError().body("Failed to delete item")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,92 +52,14 @@ 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>>>,
|
||||||
path: web::Path<(String, String)>, // (url, property)
|
|
||||||
) -> HttpResponse {
|
|
||||||
let (url, property) = path.into_inner();
|
|
||||||
log!("[API] Deleting property {} from URL {}", property, url);
|
|
||||||
let db = db.lock().await;
|
|
||||||
match db.delete_property_by_url(&url, &property).await {
|
|
||||||
Ok(_) => HttpResponse::Ok().finish(),
|
|
||||||
Err(e) => {
|
|
||||||
log!("[API] Delete error: {:?}", e);
|
|
||||||
HttpResponse::InternalServerError().body(e.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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>,
|
property: web::Path<String>,
|
||||||
) -> HttpResponse {
|
) -> HttpResponse {
|
||||||
let db = db.lock().await;
|
let db = db.lock().await;
|
||||||
match db.delete_property_by_url(&url, &property).await {
|
match db.delete_property(&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 by URL: {:?}", err);
|
leptos::logging::error!("Failed to delete property: {:?}", err);
|
||||||
HttpResponse::InternalServerError().body("Failed to delete property by URL")
|
HttpResponse::InternalServerError().body("Failed to delete property")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub async fn get_selected_properties(
|
|
||||||
db: web::Data<Arc<Mutex<Database>>>,
|
|
||||||
url: web::Path<String>,
|
|
||||||
) -> HttpResponse {
|
|
||||||
let db = db.lock().await;
|
|
||||||
match db.get_selected_properties(&url).await {
|
|
||||||
Ok(properties) => HttpResponse::Ok().json(properties),
|
|
||||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub async fn add_selected_property(
|
|
||||||
db: web::Data<Arc<Mutex<Database>>>,
|
|
||||||
url: web::Path<String>,
|
|
||||||
property: web::Json<String>,
|
|
||||||
) -> HttpResponse {
|
|
||||||
let url = url.into_inner();
|
|
||||||
let property = property.into_inner();
|
|
||||||
|
|
||||||
let db = db.lock().await;
|
|
||||||
match db.add_selected_property(&url, &property).await {
|
|
||||||
Ok(_) => HttpResponse::Ok().finish(),
|
|
||||||
Err(e) => HttpResponse::InternalServerError().body(e.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
53
src/app.rs
53
src/app.rs
|
@ -1,13 +1,11 @@
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_meta::*;
|
use leptos_meta::*;
|
||||||
use leptos_router::*;
|
use crate::components::items_list::ItemsList;
|
||||||
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 tokio::sync::mpsc;
|
||||||
use leptos::spawn_local;
|
use leptos::spawn_local;
|
||||||
// use tokio::sync::mpsc;
|
use nostr_sdk::serde_json;
|
||||||
// use crate::nostr::NostrClient;
|
|
||||||
// use nostr_sdk::serde_json;
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
|
@ -15,49 +13,26 @@ pub fn App() -> impl IntoView {
|
||||||
|
|
||||||
// Signal to manage the list of items
|
// Signal to manage the list of items
|
||||||
let (items_signal, set_items) = create_signal(Vec::<Item>::new());
|
let (items_signal, set_items) = create_signal(Vec::<Item>::new());
|
||||||
// let (tx, mut rx) = mpsc::channel::<String>(100);
|
let (tx, mut rx) = mpsc::channel::<String>(100);
|
||||||
|
|
||||||
// // Nostr client subscription for items
|
// Nostr client subscription for items
|
||||||
// spawn_local(async move {
|
spawn_local(async move {
|
||||||
// let nostr_client = NostrClient::new("wss://relay.example.com").await.unwrap();
|
let nostr_client = NostrClient::new("wss://relay.example.com").await.unwrap();
|
||||||
// nostr_client.subscribe_to_items(tx.clone()).await.unwrap();
|
nostr_client.subscribe_to_items(tx.clone()).await.unwrap();
|
||||||
|
|
||||||
// while let Some(content) = rx.recv().await {
|
while let Some(content) = rx.recv().await {
|
||||||
// if let Ok(item) = serde_json::from_str::<Item>(&content) {
|
if let Ok(item) = serde_json::from_str::<Item>(&content) {
|
||||||
// set_items.update(|items| items.push(item));
|
set_items.update(|items| items.push(item));
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
view! {
|
|
||||||
<Router>
|
|
||||||
<Routes>
|
|
||||||
<Route path="/*url" view=move || {
|
|
||||||
let location = use_location();
|
|
||||||
let current_url = move || location.pathname.get();
|
|
||||||
|
|
||||||
// Proper async handling
|
|
||||||
spawn_local({
|
|
||||||
let current_url = current_url.clone();
|
|
||||||
async move {
|
|
||||||
match load_items_from_db(¤t_url()).await {
|
|
||||||
Ok(items) => set_items.set(items),
|
|
||||||
Err(e) => log!("Error loading items: {}", e),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Stylesheet href="/assets/style.css" />
|
<Stylesheet href="/assets/style.css" />
|
||||||
<Stylesheet href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" />
|
<Stylesheet href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css" />
|
||||||
<div>
|
<div>
|
||||||
<h1>{ "CompareWare" }</h1>
|
<h1>{ "CompareWare" }</h1>
|
||||||
<ItemsList
|
<ItemsList items=items_signal set_items=set_items />
|
||||||
url=current_url()
|
|
||||||
items=items_signal
|
|
||||||
set_items=set_items />
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}/>
|
|
||||||
</Routes>
|
|
||||||
</Router>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,10 @@ 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,
|
||||||
|
@ -17,109 +19,18 @@ 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> {
|
|
||||||
//logging for the raw URL
|
|
||||||
log!("[DEBUG] Loading items for URL: {}", current_url);
|
|
||||||
|
|
||||||
let encoded_url = encode(¤t_url);
|
#[derive(Deserialize, Debug)]
|
||||||
let api_url = format!("/api/urls/{}/items", encoded_url);
|
struct DbItem {
|
||||||
|
id: String,
|
||||||
// Log the constructed API URL
|
name: String,
|
||||||
log!("[DEBUG] Making request to API endpoint: {}", api_url);
|
description: String,
|
||||||
|
wikidata_id: Option<String>,
|
||||||
let response = gloo_net::http::Request::get(&api_url)
|
custom_properties: String,
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
log!("[ERROR] Network error: {:?}", err);
|
|
||||||
format!("Failed to fetch items: {:?}", err)
|
|
||||||
})?;
|
|
||||||
// Log response metadata
|
|
||||||
log!("[DEBUG] Received response - Status: {}", response.status());
|
|
||||||
if response.status() == 200 {
|
|
||||||
log!("[DEBUG] Successfully received items");
|
|
||||||
let items = response
|
|
||||||
.json::<Vec<Item>>()
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
log!("[ERROR] JSON parsing error: {:?}", err);
|
|
||||||
format!("Failed to parse items: {:?}", err)
|
|
||||||
})?;
|
|
||||||
log!("[DEBUG] Successfully parsed {} items", items.len());
|
|
||||||
|
|
||||||
// Get the selected properties for the current URL
|
|
||||||
let selected_properties_response = gloo_net::http::Request::get(
|
|
||||||
&format!("/api/urls/{}/properties", encoded_url)
|
|
||||||
)
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
log!("[ERROR] Network error: {:?}", err);
|
|
||||||
format!("Failed to fetch selected properties: {:?}", err)
|
|
||||||
})?;
|
|
||||||
if selected_properties_response.status() == 200 {
|
|
||||||
let selected_properties: Vec<String> = selected_properties_response
|
|
||||||
.json()
|
|
||||||
.await
|
|
||||||
.map_err(|err| {
|
|
||||||
log!("[ERROR] JSON parsing error: {:?}", err);
|
|
||||||
format!("Failed to parse selected properties: {:?}", err)
|
|
||||||
})?;
|
|
||||||
log!("[DEBUG] Successfully received selected properties");
|
|
||||||
|
|
||||||
// Filter the items to only include the selected properties
|
|
||||||
let filtered_items = items
|
|
||||||
.into_iter()
|
|
||||||
.map(|item| {
|
|
||||||
let filtered_custom_properties = item
|
|
||||||
.custom_properties
|
|
||||||
.into_iter()
|
|
||||||
.filter(|(key, _)| selected_properties.contains(key))
|
|
||||||
.collect();
|
|
||||||
Item {
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
description: item.description,
|
|
||||||
wikidata_id: item.wikidata_id,
|
|
||||||
custom_properties: filtered_custom_properties,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
Ok(filtered_items)
|
|
||||||
} else {
|
|
||||||
let body = selected_properties_response.text().await.unwrap_or_default();
|
|
||||||
log!("[ERROR] Server error details:
|
|
||||||
Status: {}
|
|
||||||
URL: {}
|
|
||||||
Response Body: {}
|
|
||||||
Request URL: {}",
|
|
||||||
selected_properties_response.status(),
|
|
||||||
api_url,
|
|
||||||
body,
|
|
||||||
current_url
|
|
||||||
);
|
|
||||||
Err(format!("Server error ({}): {}", selected_properties_response.status(), body))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let body = response.text().await.unwrap_or_default();
|
|
||||||
log!("[ERROR] Server error details:
|
|
||||||
Status: {}
|
|
||||||
URL: {}
|
|
||||||
Response Body: {}
|
|
||||||
Request URL: {}",
|
|
||||||
response.status(),
|
|
||||||
api_url,
|
|
||||||
body,
|
|
||||||
current_url
|
|
||||||
);
|
|
||||||
Err(format!("Server error ({}): {}", response.status(), body))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ItemsList(
|
pub fn ItemsList(
|
||||||
url: String,
|
|
||||||
items: ReadSignal<Vec<Item>>,
|
items: ReadSignal<Vec<Item>>,
|
||||||
set_items: WriteSignal<Vec<Item>>,
|
set_items: WriteSignal<Vec<Item>>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
@ -141,29 +52,8 @@ 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());
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
spawn_local(async move {
|
||||||
fn get_current_url() -> String {
|
match load_items_from_db().await {
|
||||||
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() {
|
||||||
|
@ -217,7 +107,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
|
||||||
|
@ -226,45 +116,50 @@ 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>>, current_url: String) {
|
async fn save_item_to_db(item: Item, selected_properties: ReadSignal<HashMap<String, bool>>) {
|
||||||
|
|
||||||
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
|
||||||
custom_props
|
item.custom_properties
|
||||||
.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 ItemRequest {
|
struct ItemToSend {
|
||||||
url: String,
|
id: String,
|
||||||
item: Item,
|
name: String,
|
||||||
|
description: String,
|
||||||
|
wikidata_id: Option<String>,
|
||||||
|
custom_properties: String, // JSON-encoded string
|
||||||
}
|
}
|
||||||
|
|
||||||
log!("[FRONTEND] Saving item - ID: {}, Name: '{}', Properties: {:?}",
|
let item_to_send = ItemToSend {
|
||||||
item.id, item.name, item.custom_properties);
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
description: item.description,
|
||||||
|
wikidata_id: item.wikidata_id,
|
||||||
|
custom_properties, // Use the serialized string
|
||||||
|
};
|
||||||
|
|
||||||
let encoded_url = encode(¤t_url);
|
let response = gloo_net::http::Request::post("/api/items")
|
||||||
let api_url = format!("/api/urls/{}/items", encoded_url);
|
.json(&item_to_send)
|
||||||
|
|
||||||
let response = gloo_net::http::Request::post(&api_url)
|
|
||||||
.json(&item)
|
|
||||||
.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) => {
|
||||||
if resp.status() == 200 {
|
if resp.status() == 200 {
|
||||||
|
@ -277,20 +172,58 @@ pub fn ItemsList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_url_for_remove_item = Rc::clone(¤t_url);
|
//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 = {
|
let remove_item = move |index: usize| {
|
||||||
let set_items = set_items.clone();
|
|
||||||
move |index: usize| {
|
|
||||||
let item_id = items.get()[index].id.clone();
|
let item_id = items.get()[index].id.clone();
|
||||||
let current_url = Rc::clone(¤t_url_for_remove_item);
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let response = gloo_net::http::Request::delete(
|
let response = gloo_net::http::Request::delete(&format!("/api/items/{}", item_id))
|
||||||
&format!("/api/urls/{}/items/{}", encode(¤t_url), item_id)
|
|
||||||
)
|
|
||||||
.send()
|
.send()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
if resp.status() == 200 {
|
if resp.status() == 200 {
|
||||||
|
@ -305,24 +238,14 @@ pub fn ItemsList(
|
||||||
Err(err) => log!("Failed to delete item: {:?}", err),
|
Err(err) => log!("Failed to delete item: {:?}", err),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let current_url_for_remove_property = Rc::clone(¤t_url);
|
|
||||||
// Function to remove a property
|
// Function to remove a property
|
||||||
let remove_property = {
|
let remove_property = move |property: String| {
|
||||||
let set_custom_properties = set_custom_properties.clone();
|
|
||||||
let set_selected_properties = set_selected_properties.clone();
|
|
||||||
let set_items = set_items.clone();
|
|
||||||
move |property: String| {
|
|
||||||
let current_url = Rc::clone(¤t_url_for_remove_property);
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let response = gloo_net::http::Request::delete(
|
let response = gloo_net::http::Request::delete(&format!("/api/properties/{}", property))
|
||||||
&format!("/api/urls/{}/properties/{}", encode(¤t_url), property)
|
|
||||||
)
|
|
||||||
.send()
|
.send()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match response {
|
match response {
|
||||||
Ok(resp) => {
|
Ok(resp) => {
|
||||||
if resp.status() == 200 {
|
if resp.status() == 200 {
|
||||||
|
@ -345,7 +268,6 @@ pub fn ItemsList(
|
||||||
Err(err) => log!("Failed to delete property: {:?}", err),
|
Err(err) => log!("Failed to delete property: {:?}", err),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// State to store Wikidata suggestions
|
// State to store Wikidata suggestions
|
||||||
|
@ -512,49 +434,9 @@ pub fn ItemsList(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a new custom property
|
// Add a new custom property
|
||||||
let add_property = {
|
let add_property = move |property: String| {
|
||||||
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/", "");
|
||||||
let normalized_property_clone = normalized_property.clone();
|
|
||||||
|
|
||||||
// Check if property is already selected
|
|
||||||
if !selected_properties.get().contains_key(&normalized_property) && !normalized_property.is_empty() {
|
|
||||||
// Add property to selected properties
|
|
||||||
set_selected_properties.update(|selected| {
|
|
||||||
selected.insert(normalized_property.clone(), true);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save the selected property to the database
|
|
||||||
spawn_local({
|
|
||||||
let current_url = Rc::clone(¤t_url);
|
|
||||||
let normalized_property = normalized_property_clone.clone();
|
|
||||||
async move {
|
|
||||||
let response = gloo_net::http::Request::post(
|
|
||||||
&format!("/api/urls/{}/properties", encode(¤t_url))
|
|
||||||
)
|
|
||||||
.json(&normalized_property)
|
|
||||||
.unwrap()
|
|
||||||
.send()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match response {
|
|
||||||
Ok(resp) => {
|
|
||||||
if resp.status() == 200 {
|
|
||||||
log!("Property saved successfully");
|
|
||||||
} else {
|
|
||||||
log!("Error saving property: {}", resp.status_text());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
log!("Error saving property: {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
set_custom_properties.update(|props| {
|
set_custom_properties.update(|props| {
|
||||||
if !props.contains(&normalized_property) && !normalized_property.is_empty() {
|
if !props.contains(&normalized_property) && !normalized_property.is_empty() {
|
||||||
|
@ -572,11 +454,8 @@ 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({
|
spawn_local(async move {
|
||||||
let current_url = Rc::clone(¤t_url);
|
save_item_to_db(item_clone, selected_properties).await;
|
||||||
async move {
|
|
||||||
save_item_to_db(item_clone, selected_properties, current_url.to_string()).await;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -625,16 +504,11 @@ pub fn ItemsList(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})};
|
};
|
||||||
|
|
||||||
// Update item fields
|
// Update item fields
|
||||||
let update_item = {
|
let update_item = move |index: usize, field: &str, value: String| {
|
||||||
let set_items = set_items.clone();
|
set_items.update(|items| {
|
||||||
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" => {
|
||||||
|
@ -645,6 +519,8 @@ 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);
|
||||||
|
@ -663,11 +539,8 @@ 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({
|
spawn_local(async move {
|
||||||
let current_url = Rc::clone(¤t_url);
|
save_item_to_db(item_clone, selected_properties).await;
|
||||||
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
|
||||||
|
@ -683,16 +556,13 @@ 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({
|
spawn_local(async move {
|
||||||
let current_url = Rc::clone(¤t_url);
|
save_item_to_db(new_item, selected_properties).await;
|
||||||
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"];
|
||||||
|
@ -705,10 +575,9 @@ pub fn ItemsList(
|
||||||
<tr>
|
<tr>
|
||||||
<th>{ "Property" }</th>
|
<th>{ "Property" }</th>
|
||||||
{move || items.get().iter().enumerate().map(|(index, item)| {
|
{move || items.get().iter().enumerate().map(|(index, item)| {
|
||||||
let remove_item = remove_item.clone();
|
|
||||||
view! {
|
view! {
|
||||||
<th>
|
<th>
|
||||||
{item.name.clone()}
|
{ item.name.clone() }
|
||||||
<button on:click=move |_| remove_item(index)>{ "Delete" }</button>
|
<button on:click=move |_| remove_item(index)>{ "Delete" }</button>
|
||||||
</th>
|
</th>
|
||||||
}
|
}
|
||||||
|
@ -717,13 +586,11 @@ 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 {
|
||||||
|
@ -732,7 +599,7 @@ pub fn ItemsList(
|
||||||
<EditableCell
|
<EditableCell
|
||||||
value=item.name.clone()
|
value=item.name.clone()
|
||||||
on_input=move |value| {
|
on_input=move |value| {
|
||||||
update_item_clone(index, "name", value.clone());
|
update_item(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))
|
||||||
|
@ -794,6 +661,8 @@ 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);
|
||||||
|
@ -833,7 +702,7 @@ pub fn ItemsList(
|
||||||
"Description" => view! {
|
"Description" => view! {
|
||||||
<EditableCell
|
<EditableCell
|
||||||
value=item.description.clone()
|
value=item.description.clone()
|
||||||
on_input=move |value| update_item_clone(index, "description", value)
|
on_input=move |value| update_item(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()
|
||||||
|
@ -857,16 +726,9 @@ 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();
|
||||||
let remove_property = remove_property.clone();
|
|
||||||
custom_props.into_iter().map(move |property| {
|
custom_props.into_iter().map(move |property| {
|
||||||
let remove_property_clone = remove_property.clone();
|
|
||||||
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);
|
||||||
|
@ -877,7 +739,7 @@ pub fn ItemsList(
|
||||||
{ property_label }
|
{ property_label }
|
||||||
<button class="delete-property" on:click=move |_| {
|
<button class="delete-property" on:click=move |_| {
|
||||||
log!("Deleting property: {}", property_clone_for_button);
|
log!("Deleting property: {}", property_clone_for_button);
|
||||||
remove_property_clone(property_clone_for_button.clone());
|
remove_property(property_clone_for_button.clone());
|
||||||
set_custom_properties.update(|props| {
|
set_custom_properties.update(|props| {
|
||||||
props.retain(|p| p != &property_clone_for_button);
|
props.retain(|p| p != &property_clone_for_button);
|
||||||
});
|
});
|
||||||
|
@ -892,16 +754,14 @@ 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 update_item_cell = Arc::clone(&update_item_cell);
|
|
||||||
let property_clone_for_closure = property_clone_for_cells.clone();
|
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_cell(index, &property_clone_for_closure, value)
|
on_input=move |value| update_item(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()
|
||||||
|
@ -917,7 +777,7 @@ pub fn ItemsList(
|
||||||
}
|
}
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
}).collect::<Vec<_>>()}
|
}).collect::<Vec<_>>()
|
||||||
}}
|
}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
437
src/db.rs
437
src/db.rs
|
@ -5,9 +5,6 @@ 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, HashSet};
|
|
||||||
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)]
|
||||||
|
@ -28,84 +25,41 @@ 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,
|
||||||
FOREIGN KEY (url_id) REFERENCES urls(id) ON DELETE CASCADE
|
custom_properties TEXT
|
||||||
);",
|
);",
|
||||||
).map_err(|e| {
|
)?;
|
||||||
eprintln!("Failed creating items table: {}", e);
|
logging::log!("Database schema created or verified");
|
||||||
e
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// 4. Table for selected properties
|
|
||||||
conn.execute_batch(
|
|
||||||
"CREATE TABLE IF NOT EXISTS selected_properties (
|
|
||||||
url_id INTEGER NOT NULL,
|
|
||||||
property_id INTEGER NOT NULL,
|
|
||||||
PRIMARY KEY (url_id, property_id),
|
|
||||||
FOREIGN KEY (url_id) REFERENCES urls(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (property_id) REFERENCES properties(id) ON DELETE CASCADE
|
|
||||||
);"
|
|
||||||
).map_err(|e| {
|
|
||||||
eprintln!("Failed creating properties table: {}", e);
|
|
||||||
e
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// 5. 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 URL into the database
|
// Insert a new item into the database
|
||||||
pub async fn insert_url(&self, url: &str) -> Result<i64, Error> {
|
pub async fn insert_item(&self, item: &DbItem) -> Result<(), Error> {
|
||||||
let conn = self.conn.lock().await;
|
let conn = self.conn.lock().await;
|
||||||
let mut stmt = conn.prepare("INSERT INTO urls (url) VALUES (?)")?;
|
let wikidata_id = item.wikidata_id.as_ref().map(|s| s.as_str()).unwrap_or("");
|
||||||
let url_id = stmt.insert(&[url])?;
|
conn.execute(
|
||||||
logging::log!("URL inserted: {}", url);
|
"INSERT INTO items (id, name, description, wikidata_id, custom_properties)
|
||||||
Ok(url_id)
|
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> {
|
pub async fn delete_item(&self, item_id: &str) -> Result<(), Error> {
|
||||||
|
@ -133,6 +87,7 @@ 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();
|
||||||
|
@ -142,323 +97,6 @@ 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: Option<i64> = match conn.query_row(
|
|
||||||
"SELECT id FROM urls WHERE url = ?",
|
|
||||||
&[url],
|
|
||||||
|row| row.get(0)
|
|
||||||
) {
|
|
||||||
Ok(id) => Some(id),
|
|
||||||
Err(rusqlite::Error::QueryReturnedNoRows) => None,
|
|
||||||
Err(e) => return Err(e),
|
|
||||||
};
|
|
||||||
|
|
||||||
let url_id = match url_id {
|
|
||||||
Some(id) => id,
|
|
||||||
None => return Ok(Vec::new()), // Return empty list if URL not found
|
|
||||||
};
|
|
||||||
|
|
||||||
log!("Fetching items for URL '{}' (ID: {})", url, url_id);
|
|
||||||
|
|
||||||
|
|
||||||
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_or_create_property(
|
|
||||||
&self,
|
|
||||||
tx: &mut rusqlite::Transaction<'_>,
|
|
||||||
prop: &str
|
|
||||||
) -> Result<i64, Error> {
|
|
||||||
match tx.query_row(
|
|
||||||
"SELECT id FROM properties WHERE name = ?",
|
|
||||||
[prop],
|
|
||||||
|row| row.get::<_, i64>(0)
|
|
||||||
) {
|
|
||||||
Ok(id) => Ok(id),
|
|
||||||
Err(rusqlite::Error::QueryReturnedNoRows) => {
|
|
||||||
tx.execute("INSERT INTO properties (name) VALUES (?)", [prop])?;
|
|
||||||
Ok(tx.last_insert_rowid())
|
|
||||||
}
|
|
||||||
Err(e) => Err(e.into()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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!("[DB] Starting insert for URL: {}, Item: {}", url, item.id);
|
|
||||||
|
|
||||||
// 1. Check database lock acquisition
|
|
||||||
let lock_start = std::time::Instant::now();
|
|
||||||
let mut conn = self.conn.lock().await;
|
|
||||||
log!("[DB] Lock acquired in {:?}", lock_start.elapsed());
|
|
||||||
|
|
||||||
// 2. Transaction handling
|
|
||||||
log!("[DB] Starting transaction");
|
|
||||||
let mut tx = conn.transaction().map_err(|e| {
|
|
||||||
log!("[DB] Transaction start failed: {:?}", e);
|
|
||||||
e
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// 3. URL handling
|
|
||||||
log!("[DB] Checking URL existence: {}", url);
|
|
||||||
let url_id = match tx.query_row(
|
|
||||||
"SELECT id FROM urls WHERE url = ?",
|
|
||||||
[url],
|
|
||||||
|row| row.get::<_, i64>(0)
|
|
||||||
) {
|
|
||||||
Ok(id) => {
|
|
||||||
log!("[DB] Found existing URL ID: {}", id);
|
|
||||||
id
|
|
||||||
},
|
|
||||||
Err(rusqlite::Error::QueryReturnedNoRows) => {
|
|
||||||
log!("[DB] Inserting new URL");
|
|
||||||
tx.execute("INSERT INTO urls (url) VALUES (?)", [url])?;
|
|
||||||
let id = tx.last_insert_rowid();
|
|
||||||
log!("[DB] Created URL ID: {}", id);
|
|
||||||
id
|
|
||||||
}
|
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// 4. Item insertion
|
|
||||||
log!("[DB] Upserting item");
|
|
||||||
tx.execute(
|
|
||||||
"INSERT INTO items (id, url_id, name, description, wikidata_id)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
ON CONFLICT(id) DO UPDATE SET
|
|
||||||
url_id = excluded.url_id,
|
|
||||||
name = excluded.name,
|
|
||||||
description = excluded.description,
|
|
||||||
wikidata_id = excluded.wikidata_id",
|
|
||||||
rusqlite::params![
|
|
||||||
&item.id,
|
|
||||||
url_id,
|
|
||||||
&item.name,
|
|
||||||
&item.description,
|
|
||||||
&item.wikidata_id
|
|
||||||
],
|
|
||||||
)?;
|
|
||||||
log!("[DB] Item upserted successfully");
|
|
||||||
// Property handling with enhanced logging
|
|
||||||
log!("[DB] Synchronizing properties for item {}", item.id);
|
|
||||||
let existing_props = {
|
|
||||||
// Prepare statement and collect existing properties
|
|
||||||
let mut stmt = tx.prepare(
|
|
||||||
"SELECT p.name, ip.value
|
|
||||||
FROM item_properties ip
|
|
||||||
JOIN properties p ON ip.property_id = p.id
|
|
||||||
WHERE ip.item_id = ?"
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mapped_rows = stmt.query_map([&item.id], |row| {
|
|
||||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
mapped_rows.collect::<Result<HashMap<String, String>, _>>()?
|
|
||||||
};
|
|
||||||
|
|
||||||
for (prop, value) in &item.custom_properties {
|
|
||||||
// Update existing or insert new
|
|
||||||
let prop_id = self.get_or_create_property(&mut tx, prop).await?;
|
|
||||||
if let Some(existing_value) = existing_props.get(prop) {
|
|
||||||
if existing_value != value {
|
|
||||||
log!("[DB] Updating property {} from '{}' to '{}'", prop, existing_value, value);
|
|
||||||
tx.execute(
|
|
||||||
"UPDATE item_properties
|
|
||||||
SET value = ?
|
|
||||||
WHERE item_id = ?
|
|
||||||
AND property_id = (SELECT id FROM properties WHERE name = ?)",
|
|
||||||
rusqlite::params![value, &item.id, prop],
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log!("[DB] Adding new property {}", prop);
|
|
||||||
tx.execute(
|
|
||||||
"INSERT INTO item_properties (item_id, property_id, value)
|
|
||||||
VALUES (?, ?, ?)",
|
|
||||||
rusqlite::params![&item.id, prop_id, value],
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove deleted properties
|
|
||||||
let current_props: HashSet<&str> = item.custom_properties.keys().map(|s| s.as_str()).collect();
|
|
||||||
for (existing_prop, _) in existing_props {
|
|
||||||
if !current_props.contains(existing_prop.as_str()) {
|
|
||||||
log!("[DB] Removing deleted property {}", existing_prop);
|
|
||||||
tx.execute(
|
|
||||||
"DELETE FROM item_properties
|
|
||||||
WHERE item_id = ?
|
|
||||||
AND property_id = (SELECT id FROM properties WHERE name = ?)",
|
|
||||||
rusqlite::params![&item.id, existing_prop],
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tx.commit()?;
|
|
||||||
log!("[DB] Transaction committed successfully");
|
|
||||||
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 mut conn = self.conn.lock().await;
|
|
||||||
let tx = conn.transaction()?;
|
|
||||||
|
|
||||||
// Get URL ID
|
|
||||||
let url_id: i64 = tx.query_row(
|
|
||||||
"SELECT id FROM urls WHERE url = ?",
|
|
||||||
[url],
|
|
||||||
|row| row.get(0)
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Delete item and properties
|
|
||||||
tx.execute(
|
|
||||||
"DELETE FROM items WHERE id = ? AND url_id = ?",
|
|
||||||
[item_id, &url_id.to_string()],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
tx.commit()?;
|
|
||||||
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 mut conn = self.conn.lock().await;
|
|
||||||
let tx = conn.transaction()?;
|
|
||||||
|
|
||||||
// Get URL ID
|
|
||||||
let url_id: i64 = tx.query_row(
|
|
||||||
"SELECT id FROM urls WHERE url = ?",
|
|
||||||
[url],
|
|
||||||
|row| row.get(0)
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Delete property from all items in this URL
|
|
||||||
tx.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()],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
tx.commit()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn add_selected_property(&self, url: &str, property: &str) -> Result<(), Error> {
|
|
||||||
let mut conn = self.conn.lock().await;
|
|
||||||
let tx = conn.transaction()?;
|
|
||||||
|
|
||||||
// Get URL ID
|
|
||||||
let url_id = tx.query_row(
|
|
||||||
"SELECT id FROM urls WHERE url = ?",
|
|
||||||
[url],
|
|
||||||
|row| row.get::<_, i64>(0)
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Get/Create property
|
|
||||||
let prop_id = match tx.query_row(
|
|
||||||
"SELECT id FROM properties WHERE name = ?",
|
|
||||||
[property],
|
|
||||||
|row| row.get::<_, i64>(0)
|
|
||||||
) {
|
|
||||||
Ok(id) => id,
|
|
||||||
Err(_) => {
|
|
||||||
tx.execute("INSERT INTO properties (name) VALUES (?)", [property])?;
|
|
||||||
tx.last_insert_rowid()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Insert into selected_properties
|
|
||||||
tx.execute(
|
|
||||||
"INSERT OR IGNORE INTO selected_properties (url_id, property_id) VALUES (?, ?)",
|
|
||||||
[url_id, prop_id]
|
|
||||||
)?;
|
|
||||||
|
|
||||||
tx.commit()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_selected_properties(&self, url: &str) -> Result<Vec<String>, Error> {
|
|
||||||
let conn = self.conn.lock().await;
|
|
||||||
let mut stmt = conn.prepare(
|
|
||||||
"SELECT p.name
|
|
||||||
FROM selected_properties sp
|
|
||||||
JOIN properties p ON sp.property_id = p.id
|
|
||||||
JOIN urls u ON sp.url_id = u.id
|
|
||||||
WHERE u.url = ?"
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let properties = stmt.query_map([url], |row| row.get(0))?;
|
|
||||||
properties.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -468,8 +106,31 @@ 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};
|
pub use db_impl::{Database, DbItem, ItemResponse};
|
97
src/main.rs
97
src/main.rs
|
@ -1,11 +1,4 @@
|
||||||
#[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, get_selected_properties, add_selected_property};
|
|
||||||
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;
|
||||||
|
@ -13,21 +6,19 @@ 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;
|
use compareware::db::{Database, DbItem};
|
||||||
use compareware::api::{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;
|
||||||
|
|
||||||
// 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
|
// Load configuration
|
||||||
let conf = get_configuration(None).await.unwrap();
|
let conf = get_configuration(None).await.unwrap();
|
||||||
let addr = conf.leptos_options.site_addr;
|
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
|
||||||
|
|
||||||
// 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);
|
||||||
|
@ -41,19 +32,13 @@ 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")
|
||||||
.service(
|
.route("/items", web::get().to(get_items)) // GET /api/items
|
||||||
web::scope("/urls/{url}")
|
.route("/items", web::post().to(create_item)) // POST /api/items
|
||||||
.route("/items", web::get().to(get_items_handler)) // GET items by URL
|
.route("/items/{id}", web::delete().to(delete_item)) // DELETE /api/items/{id}
|
||||||
.route("/items", web::post().to(create_item_handler)) // Create item for URL
|
.route("/properties/{property}", web::delete().to(delete_property)), // DELETE /api/properties/{property}
|
||||||
.route("/items/{item_id}", web::delete().to(delete_item)) // Delete item for URL
|
|
||||||
.route("/properties", web::get().to(get_selected_properties_handler))
|
|
||||||
.route("/properties", web::post().to(add_selected_property_handler))
|
|
||||||
.route("/properties/{property}", web::delete().to(delete_property)) // Delete property for URL
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
// Register server functions
|
// Register server functions
|
||||||
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
.route("/api/{tail:.*}", leptos_actix::handle_server_fns())
|
||||||
|
@ -70,74 +55,12 @@ 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")]
|
|
||||||
async fn get_selected_properties_handler(
|
|
||||||
db: web::Data<Arc<Mutex<Database>>>,
|
|
||||||
url: web::Path<String>,
|
|
||||||
) -> impl Responder {
|
|
||||||
get_selected_properties(db, url).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
async fn add_selected_property_handler(
|
|
||||||
db: web::Data<Arc<Mutex<Database>>>,
|
|
||||||
url: web::Path<String>,
|
|
||||||
property: web::Json<String>,
|
|
||||||
) -> impl Responder {
|
|
||||||
add_selected_property(db, url, property).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,6 +7,13 @@ 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