From 02bfc8afc755981bda301b2c5a49229bfee3150c Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 20 Oct 2023 04:21:48 +0200 Subject: [PATCH] feat: added leptos web frontend poc --- Cargo.toml | 12 ++++ index.html | 68 +++++++++++++++++++++ src/web/main.rs | 154 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 index.html create mode 100644 src/web/main.rs diff --git a/Cargo.toml b/Cargo.toml index 2825b4a..5459c61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,11 @@ name = "fedicharter-cli" path = "src/cli/main.rs" required-features = ["cli"] +[[bin]] +name = "fedicharter-web" +path = "src/web/main.rs" +required-features = ["web"] + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -37,8 +42,15 @@ reqwest = { version = "0.11.20", features = ["json"], optional = true } sea-orm = { version = "0.12.3", features = ["runtime-tokio-native-tls", "sqlx-sqlite", "sqlx-postgres"], optional = true } clap = { version = "4.4.6", features = ["derive"], optional = true } +# wasm and web stuff +console_error_panic_hook = { version = "0.1.7", optional = true } +console_log = { version = "1.0.0", optional = true } +leptos = { version = "0.5.1", features = ["csr"], optional = true } +reqwasm = { version = "0.5.0", optional = true } + [features] default = ["web", "cli"] db = ["dep:tokio", "dep:sea-orm", "dep:reqwest"] +web = ["dep:console_error_panic_hook", "dep:console_log", "dep:leptos", "dep:reqwasm"] cli = ["db", "dep:axum", "dep:tracing-subscriber", "dep:clap"] diff --git a/index.html b/index.html new file mode 100644 index 0000000..5265f71 --- /dev/null +++ b/index.html @@ -0,0 +1,68 @@ + + + + akkoma bubble network + + + + +
+

crunching fedi network data, be patient...

+
+ + + diff --git a/src/web/main.rs b/src/web/main.rs new file mode 100644 index 0000000..7ced43b --- /dev/null +++ b/src/web/main.rs @@ -0,0 +1,154 @@ +use std::sync::Arc; + +use leptos::{*, error::Result}; +use fedicharter::nodeinfo::model::NodeInfoOwned; +use wasm_bindgen::JsCast; + +fn main() { + _ = console_log::init_with_level(log::Level::Debug); + console_error_panic_hook::set_once(); + mount_to( + document().get_element_by_id("#app") + .expect("no #app element") + .unchecked_into(), // lmao i found it like this on leptops examples + || view! { } + ); +} + +async fn fetch_node_info(domain: String) -> Result<(Arc, String)> { + Ok(( + Arc::new( + reqwasm::http::Request::get( + &format!("https://api.alemi.dev/akkoma/bubble/instance?domain={}", &domain) + ).send() + .await? + .json::() + .await? + ), + domain, + )) +} + +#[component] +fn App() -> impl IntoView { + // let (live, live_tx) = create_signal("://".to_string()); + let (submitted, submitted_tx) = create_signal("social.alemi.dev".to_string()); + + let nodeinfo = create_local_resource(move || submitted.get(), fetch_node_info); + + let fallback = move |errors: RwSignal| { + let error_list = move || { + errors.with(|errors| { + errors + .iter() + .map(|(_, e)| view! {
  • {e.to_string()}
  • }) + .collect_view() + }) + }; + + view! { +
    +

    "Error"

    +
      {error_list}
    +
    + } + }; + + // the renderer can handle Option<_> and Result<_> states + // by displaying nothing for None if the resource is still loading + // and by using the ErrorBoundary fallback to catch Err(_) + // so we'll just use `.and_then()` to map over the happy path + let instance_view = move || { + nodeinfo.and_then(|(data, domain)| { + view! { } + }) + }; + + view ! { + + + "Loading (Suspense Fallback)..." } + }> +
    + {instance_view} +
    +
    +
    + } +} + +#[component] +fn Searchbar( + #[prop(optional)] + live: Option>, + #[prop(optional)] + submitted: Option>, + #[prop(default = "".to_string())] + placeholder: String, +) -> impl IntoView { + let input_element: NodeRef = create_node_ref(); + + let on_submit = move |ev: web_sys::SubmitEvent| { + ev.prevent_default(); + if let Some(tx) = submitted { + tx.set( + input_element.get() + .expect("no input field") + .value() + ); + } + }; + + view! { +
    +
    + + +
    +
    + } +} + +#[component] +fn InstanceDetails( + domain: String, + instance: Arc, +) -> impl IntoView { + let title = instance.metadata.get("nodeName") + .map(|x| x.as_str().expect("name is not a string").to_string()) + .unwrap_or(domain); + view! { +
    +

    { &title }

    +

    "["{instance.protocols.join(",")}"]~ " { &instance.software.name }1 " v" { &instance.software.version }src

    +
      +
    • "users:" +
        +
      • "registered: " { instance.usage.users.total }
      • +
      • "active monthly: " { instance.usage.users.active_month }
      • +
      • "active half year: " { instance.usage.users.active_halfyear }
      • +
      +
    • +
    • "posts: " { instance.usage.local_posts }
    • +
    • "comments: " { instance.usage.local_comments }
    • +
    • "open registration: " { instance.open_registrations }
    • +
    • "services:" +
        +
      • "rx: " "["{ instance.services.inbound.join(", ") }"]"
      • +
      • "tx: " "["{ instance.services.outbound.join(", ") }"]"
      • +
      +
    • +
    • "metadata: "
      { serde_json::to_string_pretty(&instance.metadata) }
    • +
    +
    + } +}