at main 207 lines 7.1 kB view raw
1//! Subdomain Dioxus application. 2//! 3//! Separate router for subdomain hosting with simpler route structure. 4 5use dioxus::prelude::*; 6use jacquard::smol_str::{SmolStr, ToSmolStr}; 7use jacquard::types::string::AtIdentifier; 8 9use crate::components::identity::RepositoryIndex; 10use crate::components::{EntryPage, NotebookCss}; 11use crate::host_mode::SubdomainContext; 12use crate::views::{NotebookEntryByRkey, NotebookEntryEdit, NotebookIndex, SubdomainNavbar}; 13 14/// Subdomain route enum - simpler paths without /:ident/:notebook prefix. 15#[derive(Debug, Clone, Routable, PartialEq)] 16#[rustfmt::skip] 17pub enum SubdomainRoute { 18 #[layout(SubdomainNavbar)] 19 /// Landing page - custom entry or notebook index. 20 #[route("/")] 21 SubdomainLanding {}, 22 /// Explicit notebook index. 23 #[route("/index")] 24 SubdomainIndexPage {}, 25 /// Entry by title. 26 #[route("/:title")] 27 SubdomainEntry { title: SmolStr }, 28 /// Entry by rkey. 29 #[route("/e/:rkey")] 30 SubdomainEntryByRkey { rkey: SmolStr }, 31 /// Entry edit by rkey. 32 #[route("/e/:rkey/edit")] 33 SubdomainEntryEdit { rkey: SmolStr }, 34 /// Profile/repository view. 35 #[route("/u/:ident")] 36 SubdomainProfile { ident: AtIdentifier<'static> }, 37} 38 39/// Look up notebook by global path and build SubdomainContext. 40pub async fn lookup_subdomain_context( 41 fetcher: &crate::fetch::Fetcher, 42 path: &str, 43) -> Option<SubdomainContext> { 44 use jacquard::IntoStatic; 45 use jacquard::smol_str::SmolStr; 46 use jacquard::xrpc::XrpcClient; 47 use weaver_api::sh_weaver::notebook::resolve_global_notebook::ResolveGlobalNotebook; 48 49 let request = ResolveGlobalNotebook::new().path(path).build(); 50 51 match fetcher.send(request).await { 52 Ok(response) => match response.into_output() { 53 Ok(output) => { 54 let notebook = output.notebook; 55 56 let owner = notebook.uri.authority().clone().into_static(); 57 let Some(rkey) = notebook.uri.rkey() else { 58 tracing::warn!(path, uri = %notebook.uri, "Notebook URI missing rkey"); 59 return None; 60 }; 61 let rkey = rkey.0.to_smolstr(); 62 let notebook_path = notebook 63 .path 64 .map(|p| SmolStr::new(p.as_ref())) 65 .unwrap_or_else(|| SmolStr::new(path)); 66 67 tracing::info!(path, %owner, %rkey, "Notebook lookup succeeded"); 68 Some(SubdomainContext { 69 owner, 70 notebook_path, 71 notebook_rkey: rkey, 72 notebook_title: notebook.title.clone().unwrap_or_default().to_smolstr(), 73 }) 74 } 75 Err(e) => { 76 tracing::warn!(path, error = %e, "Failed to parse notebook response"); 77 None 78 } 79 }, 80 Err(e) => { 81 tracing::warn!(path, error = %e, "Global notebook lookup request failed"); 82 None 83 } 84 } 85} 86 87/// Extract subdomain from host if it matches base domain pattern. 88pub fn extract_subdomain(host: &str, base: &str) -> Option<String> { 89 let suffix = format!(".{}", base); 90 if host.ends_with(&suffix) && host.len() > suffix.len() { 91 Some(host[..host.len() - suffix.len()].to_string()) 92 } else { 93 None 94 } 95} 96 97const ENTRY_CSS: Asset = asset!("/assets/styling/entry.css"); 98const LAYOUTS_CSS: Asset = asset!("/assets/styling/layouts.css"); 99 100/// Root component for subdomain app. 101#[component] 102pub fn SubdomainApp() -> Element { 103 tracing::info!("SubdomainApp: rendering root"); 104 rsx! { 105 document::Link { rel: "icon", href: crate::FAVICON } 106 document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" } 107 document::Link { rel: "preconnect", href: "https://fonts.gstatic.com" } 108 document::Link { rel: "stylesheet", href: crate::THEME_DEFAULTS_CSS } 109 document::Link { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=IBM+Plex+Serif:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" } 110 document::Link { rel: "stylesheet", href: crate::MAIN_CSS } 111 document::Link { rel: "stylesheet", href: LAYOUTS_CSS } 112 document::Link { rel: "stylesheet", href: ENTRY_CSS } 113 crate::components::toast::ToastProvider { 114 Router::<SubdomainRoute> {} 115 } 116 } 117} 118 119/// Landing page - check for custom "/" entry, else show notebook index. 120#[component] 121fn SubdomainLanding() -> Element { 122 tracing::info!("SubdomainLanding: start"); 123 let ctx = use_context::<SubdomainContext>(); 124 tracing::info!("SubdomainLanding: got context, rendering"); 125 126 // TODO: Check for entry with custom path "/" for this notebook. 127 // For now, just render the notebook index. 128 rsx! { 129 130 NotebookCss { ident: ctx.owner_ident().to_smolstr(), notebook: ctx.notebook_path.clone() } 131 NotebookIndex { 132 ident: ctx.owner_ident(), 133 book_title: ctx.notebook_title.clone(), 134 } 135 } 136} 137 138/// Explicit notebook index route. 139#[component] 140fn SubdomainIndexPage() -> Element { 141 let ctx = use_context::<SubdomainContext>(); 142 143 rsx! { 144 145 NotebookCss { ident: ctx.owner_ident().to_smolstr(), notebook: ctx.notebook_path.clone() } 146 NotebookIndex { 147 ident: ctx.owner_ident(), 148 book_title: ctx.notebook_title.clone(), 149 } 150 } 151} 152 153/// Entry by title. 154#[component] 155fn SubdomainEntry(title: SmolStr) -> Element { 156 let ctx = use_context::<SubdomainContext>(); 157 158 rsx! { 159 160 NotebookCss { ident: ctx.owner_ident().to_smolstr(), notebook: ctx.notebook_path.clone() } 161 EntryPage { 162 ident: ctx.owner_ident(), 163 book_title: ctx.notebook_title.clone(), 164 title: title, 165 } 166 } 167} 168 169/// Entry by rkey. 170#[component] 171fn SubdomainEntryByRkey(rkey: SmolStr) -> Element { 172 let ctx = use_context::<SubdomainContext>(); 173 174 rsx! { 175 176 NotebookCss { ident: ctx.owner_ident().to_smolstr(), notebook: ctx.notebook_path.clone() } 177 NotebookEntryByRkey { 178 ident: ctx.owner_ident(), 179 book_title: ctx.notebook_title.clone(), 180 rkey: rkey, 181 } 182 } 183} 184 185/// Entry edit by rkey. 186#[component] 187fn SubdomainEntryEdit(rkey: SmolStr) -> Element { 188 let ctx = use_context::<SubdomainContext>(); 189 190 rsx! { 191 192 NotebookCss { ident: ctx.owner_ident().to_smolstr(), notebook: ctx.notebook_path.clone() } 193 NotebookEntryEdit { 194 ident: ctx.owner_ident(), 195 book_title: ctx.notebook_title.clone(), 196 rkey: rkey, 197 } 198 } 199} 200 201/// Profile/repository view for an identity. 202#[component] 203fn SubdomainProfile(ident: AtIdentifier<'static>) -> Element { 204 rsx! { 205 RepositoryIndex { ident } 206 } 207}