editable-cells-wiki #1

Merged
ryanmwangi merged 3 commits from editable-cells-wiki into main 2024-12-23 15:01:23 +00:00
6 changed files with 272 additions and 119 deletions
Showing only changes of commit 16b04fcc1e - Show all commits

View File

@ -1,7 +1,7 @@
use leptos::*; use leptos::*;
use leptos_meta::*; use leptos_meta::*;
use crate::components::items_list::ItemsList; use crate::components::items_list::ItemsList;
use crate::models::item::Item; use crate::models::item::{Item, ReviewWithRating};
use crate::nostr::NostrClient; use crate::nostr::NostrClient;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use uuid::Uuid; use uuid::Uuid;
@ -28,28 +28,25 @@ pub fn App() -> impl IntoView {
} }
}); });
// Function to add a new item from the grid // Handle adding a new item
let add_item_from_grid = move || { let add_item = move || {
let new_id = Uuid::new_v4().to_string(); let new_item = Item {
id: Uuid::new_v4().to_string(),
set_items.update(|items| { name: "New Item".to_string(),
let item = Item { description: String::new(),
id: new_id.clone(), tags: vec![],
name: String::new(), reviews: vec![],
description: String::new(), wikidata_id: None,
tags: vec![], };
reviews: vec![], set_items.update(|items| items.push(new_item));
wikidata_id: None,
};
items.push(item);
});
}; };
view! { view! {
<Stylesheet href="/assets/style.css" /> <Stylesheet href="/assets/style.css" />
<div> <div>
<h1>{ "CompareWare" }</h1> <h1>{ "CompareWare" }</h1>
<ItemsList items=items_signal set_items=set_items on_add_item=add_item_from_grid /> <button on:click=move |_| add_item()>{ "Add New Item" }</button>
<ItemsList items=items_signal set_items=set_items />
</div> </div>
} }
} }

View File

@ -0,0 +1,23 @@
use leptos::*;
#[component]
pub fn EditableCell(
value: String,
on_input: impl Fn(String) + 'static,
) -> impl IntoView {
let (input_value, set_input_value) = create_signal(value.clone());
let handle_input = move |e: web_sys::Event| {
let new_value = event_target_value(&e);
set_input_value(new_value.clone());
on_input(new_value);
};
view! {
<input
type="text"
value={input_value.get()}
on:input=handle_input
/>
}
}

View File

@ -1,144 +1,182 @@
use crate::models::item::Item; use crate::components::editable_cell::EditableCell;
use gloo_net::http::Request; use crate::components::tag_editor::TagEditor;
use leptos::logging::log;
use leptos::*; use leptos::*;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use uuid::Uuid;
use wasm_bindgen_futures::spawn_local;
#[derive(Deserialize, Clone, Debug)] #[derive(Deserialize, Clone, Debug)]
struct WikidataResponse { struct WikidataSuggestion {
entities: HashMap<String, WikidataEntity>, id: String,
label: String,
description: Option<String>,
} }
#[derive(Deserialize, Clone, Debug)] #[derive(Clone, Debug)]
struct WikidataEntity { struct Item {
labels: Option<HashMap<String, WikidataLabel>>, id: String,
descriptions: Option<HashMap<String, WikidataLabel>>, name: String,
} description: String,
tags: Vec<(String, String)>,
#[derive(Deserialize, Clone, Debug)] wikidata_id: Option<String>,
struct WikidataLabel {
value: String,
} }
#[component] #[component]
pub fn ItemsList(items: ReadSignal<Vec<Item>>, set_items: WriteSignal<Vec<Item>>, on_add_item: impl Fn() + 'static, ) -> impl IntoView { pub fn ItemsList(
let (wikidata_data, set_wikidata_data) = create_signal(Vec::<Option<WikidataEntity>>::new()); items: ReadSignal<Vec<Item>>,
set_items: WriteSignal<Vec<Item>>,
) -> impl IntoView {
let (wikidata_suggestions, set_wikidata_suggestions) =
create_signal(Vec::<WikidataSuggestion>::new());
// Fetch data from Wikidata for a given item name // Fetch Wikidata suggestions
let fetch_wikidata = move |item_name: String| { let fetch_wikidata_suggestions = move |query: String| {
spawn_local(async move { spawn_local(async move {
let url = format!("https://www.wikidata.org/w/api.php?action=wbsearchentities&search={}&language=en&limit=1&format=json", item_name); if query.is_empty() {
match Request::get(&url).send().await { set_wikidata_suggestions(Vec::new());
Ok(response) => match response.json::<WikidataResponse>().await { return;
Ok(parsed_data) => { }
if let Some(entity) = parsed_data.entities.values().next() {
set_wikidata_data.update(|current_data| { let url = format!(
current_data.push(Some(entity.clone())); "https://www.wikidata.org/w/api.php?action=wbsearchentities&search={}&language=en&limit=5&format=json&origin=*",
}); query
} );
match gloo_net::http::Request::get(&url).send().await {
Ok(response) => {
if let Ok(data) = response.json::<WikidataResponse>().await {
set_wikidata_suggestions(data.search);
} }
Err(_) => log!("Failed to parse response from Wikidata"), }
}, Err(_) => log!("Failed to fetch Wikidata suggestions"),
Err(_) => log!("Failed to make request to Wikidata"),
} }
}); });
}; };
// Handle updating grid cells // Update item fields
let update_item = move |index: usize, column: &str, value: String| { let update_item = move |index: usize, field: &str, value: String| {
set_items.update(|items| { set_items.update(|items| {
if let Some(item) = items.get_mut(index) { if let Some(item) = items.get_mut(index) {
match column { match field {
"name" => item.name = value, "name" => {
"description" => item.description = value, item.name = value.clone();
"tags" => item.tags.push((value.clone(), value)), // For simplicity, adding the same value as key and value. fetch_wikidata_suggestions(value);
"review" => item.reviews.push(crate::models::item::ReviewWithRating {
content: value.clone(),
rating: 5, // Assuming a default rating of 5
}),
"rating" => {
if let Ok(rating) = value.parse::<u8>() {
item.reviews.last_mut().map(|r| r.rating = rating);
}
} }
"description" => item.description = value,
_ => (), _ => (),
} }
} }
}); });
}; };
// Trigger add item event // Add a new tag to an item
let add_item = move |_| { let add_tag = move |index: usize, key: String, value: String| {
on_add_item(); // Call the passed closure from App to add a new item set_items.update(|items| {
if let Some(item) = items.get_mut(index) {
item.tags.push((key, value));
}
});
};
// Remove a tag from an item
let remove_tag = move |item_index: usize, tag_index: usize| {
set_items.update(|items| {
if let Some(item) = items.get_mut(item_index) {
item.tags.remove(tag_index);
}
});
};
// Add a new item
let add_item = move || {
set_items.update(|items| {
items.push(Item {
id: Uuid::new_v4().to_string(),
name: String::new(),
description: String::new(),
tags: vec![],
wikidata_id: None,
});
});
};
// Remove an item
let remove_item = move |index: usize| {
set_items.update(|items| {
items.remove(index);
});
}; };
view! { view! {
<div> <div>
<h1>{ "CompareWare" }</h1> <h1>{ "Items List" }</h1>
<button on:click=add_item>{ "Add New Item" }</button> <button on:click=add_item>{ "Add New Item" }</button>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>{ "Name" }</th> <th>{ "Name" }</th>
<th>{ "Description" }</th> <th>{ "Description" }</th>
<th>{ "Tags" }</th> <th>{ "Tags" }</th>
<th>{ "Review" }</th> <th>{ "Actions" }</th>
<th>{ "Rating" }</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{move || { {move || items.get().iter().enumerate().map(|(index, item)| {
items.get().iter().enumerate().map(|(index, item)| { view! {
view! { <tr>
<tr> // Editable Name Field with Wikidata Integration
<td> <td>
<input <EditableCell
type="text" value=item.name.clone()
value={item.name.clone()} on_input=move |value| update_item(index, "name", value)
on:input=move |e| { />
update_item(index, "name", event_target_value(&e)); <ul>
fetch_wikidata(event_target_value(&e)); // Fetch Wikidata when name is entered {move || {
} wikidata_suggestions.get().iter().map(|suggestion| {
/> view! {
</td> <li on:click=move |_| {
<td> set_items.update(|items| {
<input if let Some(item) = items.get_mut(index) {
type="text" item.wikidata_id = Some(suggestion.id.clone());
value={item.description.clone()} item.name = suggestion.label.clone();
on:input=move |e| update_item(index, "description", event_target_value(&e)) }
/> });
</td> }}>
<td> { format!("{} - {}", suggestion.label, suggestion.description.clone().unwrap_or_default()) }
<input </li>
type="text" }
placeholder="Add tags" }).collect::<Vec<_>>()
on:input=move |e| update_item(index, "tags", event_target_value(&e)) }}
/> </ul>
</td> </td>
<td> // Editable Description Field
<textarea <td>
value={item.reviews.iter().map(|review| format!("{}: {}", review.rating, review.content)).collect::<Vec<_>>().join("\n")} <EditableCell
on:input=move |e| update_item(index, "review", event_target_value(&e)) value=item.description.clone()
/> on_input=move |value| update_item(index, "description", value)
</td> />
<td> </td>
<input // Tag Editor
type="number" <td>
value={item.reviews.last().map(|r| r.rating).unwrap_or(5)} <TagEditor
min="1" max="5" tags=item.tags.clone()
on:input=move |e| update_item(index, "rating", event_target_value(&e)) on_add=move |key, value| add_tag(index, key, value)
/> on_remove=move |tag_index| remove_tag(index, tag_index)
</td> />
</tr> </td>
} // Actions
}).collect::<Vec<_>>() <td>
}} <button on:click=move |_| remove_item(index)>{ "Delete" }</button>
</td>
</tr>
}
}).collect::<Vec<_>>()}
</tbody> </tbody>
</table> </table>
</div> </div>
} }
} }
#[derive(Deserialize, Clone, Debug)]
struct WikidataResponse {
search: Vec<WikidataSuggestion>,
}

View File

@ -1,2 +1,4 @@
pub mod item_form; pub mod item_form;
pub mod items_list; pub mod items_list;
pub mod editable_cell;
pub mod tag_editor;

View File

@ -0,0 +1,45 @@
use leptos::*;
#[component]
pub fn TagEditor(
tags: Vec<(String, String)>,
on_add: impl Fn(String, String) + 'static,
on_remove: impl Fn(usize) + 'static,
) -> impl IntoView {
let (key, set_key) = create_signal(String::new());
let (value, set_value) = create_signal(String::new());
let add_tag = move || {
if !key.get().is_empty() && !value.get().is_empty() {
on_add(key.get(), value.get());
set_key(String::new());
set_value(String::new());
}
};
view! {
<div>
<ul>
{tags.iter().enumerate().map(|(index, (k, v))| {
view! {
<li>
{format!("{}: {}", k, v)}
<button on:click=move |_| on_remove(index)>{ "Remove" }</button>
</li>
}
}).collect::<Vec<_>>()}
</ul>
<input
placeholder="Key"
value={key.get()}
on:input=move |e| set_key(event_target_value(&e))
/>
<input
placeholder="Value"
value={value.get()}
on:input=move |e| set_value(event_target_value(&e))
/>
<button on:click=add_tag>{ "Add Tag" }</button>
</div>
}
}

View File

@ -0,0 +1,48 @@
use leptos::*;
use serde::Deserialize;
#[derive(Deserialize, Clone, Debug)]
struct WikidataResult {
id: String,
label: String,
description: Option<String>,
}
#[component]
pub fn WikidataLookup(
query: String,
on_select: impl Fn(WikidataResult) + 'static,
) -> impl IntoView {
let (suggestions, set_suggestions) = create_signal(Vec::new());
let fetch_suggestions = move |query: String| {
spawn_local(async move {
if query.is_empty() {
set_suggestions(Vec::new());
return;
}
let url = format!("https://www.wikidata.org/w/api.php?action=wbsearchentities&search={}&language=en&limit=5&format=json&origin=*", query);
if let Ok(response) = reqwest::get(&url).await {
if let Ok(data) = response.json::<WikidataResponse>().await {
set_suggestions(data.search);
}
}
});
};
create_effect(move || {
fetch_suggestions(query.clone());
});
view! {
<ul>
{suggestions.get().iter().map(|suggestion| {
view! {
<li on:click=move |_| on_select(suggestion.clone())>
{format!("{} - {}", suggestion.label, suggestion.description.clone().unwrap_or_default())}
</li>
}
}).collect::<Vec<_>>()}
</ul>
}
}