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! {
+
+ }
+ };
+
+ // 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
+
+
+ }
+}