at main 411 lines 17 kB view raw
1//! Router-agnostic link and navigation for shared components. 2//! 3//! AppLink dispatches to either `Link<Route>` or `Link<SubdomainRoute>` based on 4//! the current LinkMode context, preserving proper client-side navigation semantics. 5//! 6//! AppNavigate provides programmatic navigation that dispatches similarly. 7 8use crate::env::WEAVER_APP_HOST; 9use crate::host_mode::LinkMode; 10use crate::{CustomDomainRoute, Route, SubdomainRoute}; 11use dioxus::prelude::*; 12use jacquard::smol_str::SmolStr; 13use jacquard::types::string::AtIdentifier; 14 15/// Target for router-agnostic links. 16#[derive(Clone, PartialEq)] 17pub enum AppLinkTarget { 18 /// Entry by title path: /:ident/:book/:title or /:title 19 Entry { 20 ident: AtIdentifier<'static>, 21 book_title: SmolStr, 22 entry_path: SmolStr, 23 }, 24 /// Entry by rkey: /:ident/:book/e/:rkey or /e/:rkey 25 EntryByRkey { 26 ident: AtIdentifier<'static>, 27 book_title: SmolStr, 28 rkey: SmolStr, 29 }, 30 /// Entry edit: /:ident/:book/e/:rkey/edit or /e/:rkey/edit 31 EntryEdit { 32 ident: AtIdentifier<'static>, 33 book_title: SmolStr, 34 rkey: SmolStr, 35 }, 36 /// Notebook index: /:ident/:book or / 37 Notebook { 38 ident: AtIdentifier<'static>, 39 book_title: SmolStr, 40 }, 41 /// Profile/repository: /:ident or /u/:ident 42 Profile { ident: AtIdentifier<'static> }, 43 /// Standalone entry: /:ident/e/:rkey (always main domain in subdomain mode) 44 StandaloneEntry { 45 ident: AtIdentifier<'static>, 46 rkey: SmolStr, 47 }, 48 /// Standalone entry edit: /:ident/e/:rkey/edit 49 StandaloneEntryEdit { 50 ident: AtIdentifier<'static>, 51 rkey: SmolStr, 52 }, 53 /// New draft: /:ident/new?notebook=... 54 NewDraft { 55 ident: AtIdentifier<'static>, 56 notebook: Option<SmolStr>, 57 }, 58 /// Drafts list: /:ident/drafts 59 Drafts { ident: AtIdentifier<'static> }, 60 /// Invites page: /:ident/invites 61 Invites { ident: AtIdentifier<'static> }, 62} 63 64#[derive(Props, Clone, PartialEq)] 65pub struct AppLinkProps { 66 pub to: AppLinkTarget, 67 #[props(default)] 68 pub class: Option<String>, 69 pub children: Element, 70} 71 72/// Router-agnostic link component. 73/// 74/// Renders the appropriate `Link<Route>` or `Link<SubdomainRoute>` based on LinkMode context. 75#[component] 76pub fn AppLink(props: AppLinkProps) -> Element { 77 let link_mode = use_context::<LinkMode>(); 78 let class = props.class.clone().unwrap_or_default(); 79 80 match link_mode { 81 LinkMode::MainDomain => { 82 let route = match props.to.clone() { 83 AppLinkTarget::Entry { 84 ident, 85 book_title, 86 entry_path, 87 } => Route::EntryPage { 88 ident, 89 book_title, 90 title: entry_path, 91 }, 92 AppLinkTarget::EntryByRkey { 93 ident, 94 book_title, 95 rkey, 96 } => Route::NotebookEntryByRkey { 97 ident, 98 book_title, 99 rkey, 100 }, 101 AppLinkTarget::EntryEdit { 102 ident, 103 book_title, 104 rkey, 105 } => Route::NotebookEntryEdit { 106 ident, 107 book_title, 108 rkey, 109 }, 110 AppLinkTarget::Notebook { ident, book_title } => { 111 Route::NotebookIndex { ident, book_title } 112 } 113 AppLinkTarget::Profile { ident } => Route::RepositoryIndex { ident }, 114 AppLinkTarget::StandaloneEntry { ident, rkey } => { 115 Route::StandaloneEntry { ident, rkey } 116 } 117 AppLinkTarget::StandaloneEntryEdit { ident, rkey } => { 118 Route::StandaloneEntryEdit { ident, rkey } 119 } 120 AppLinkTarget::NewDraft { ident, notebook } => Route::NewDraft { ident, notebook }, 121 AppLinkTarget::Drafts { ident } => Route::DraftsList { ident }, 122 AppLinkTarget::Invites { ident } => Route::InvitesPage { ident }, 123 }; 124 rsx! { 125 Link { to: route, class: "{class}", {props.children} } 126 } 127 } 128 LinkMode::Subdomain => { 129 // For subdomain mode, some links go to SubdomainRoute, others to main domain 130 match props.to.clone() { 131 AppLinkTarget::Entry { entry_path, .. } => { 132 let route = SubdomainRoute::SubdomainEntry { title: entry_path }; 133 rsx! { 134 Link { to: route, class: "{class}", {props.children} } 135 } 136 } 137 AppLinkTarget::EntryByRkey { rkey, .. } => { 138 let route = SubdomainRoute::SubdomainEntryByRkey { rkey }; 139 rsx! { 140 Link { to: route, class: "{class}", {props.children} } 141 } 142 } 143 AppLinkTarget::EntryEdit { rkey, .. } => { 144 let route = SubdomainRoute::SubdomainEntryEdit { rkey }; 145 rsx! { 146 Link { to: route, class: "{class}", {props.children} } 147 } 148 } 149 AppLinkTarget::Notebook { .. } => { 150 let route = SubdomainRoute::SubdomainLanding {}; 151 rsx! { 152 Link { to: route, class: "{class}", {props.children} } 153 } 154 } 155 AppLinkTarget::Profile { ident } => { 156 let route = SubdomainRoute::SubdomainProfile { ident }; 157 rsx! { 158 Link { to: route, class: "{class}", {props.children} } 159 } 160 } 161 // These go to main domain in subdomain mode 162 AppLinkTarget::StandaloneEntry { ident, rkey } => { 163 let href = format!("{}/{}/e/{}", WEAVER_APP_HOST, ident, rkey); 164 rsx! { 165 a { href: "{href}", class: "{class}", {props.children} } 166 } 167 } 168 AppLinkTarget::StandaloneEntryEdit { ident, rkey } => { 169 let href = format!("{}/{}/e/{}/edit", WEAVER_APP_HOST, ident, rkey); 170 rsx! { 171 a { href: "{href}", class: "{class}", {props.children} } 172 } 173 } 174 AppLinkTarget::NewDraft { ident, notebook } => { 175 let href = match notebook { 176 Some(nb) => format!("{}/{}/new?notebook={}", WEAVER_APP_HOST, ident, nb), 177 None => format!("{}/{}/new", WEAVER_APP_HOST, ident), 178 }; 179 rsx! { 180 a { href: "{href}", class: "{class}", {props.children} } 181 } 182 } 183 AppLinkTarget::Drafts { ident } => { 184 let href = format!("{}/{}/drafts", WEAVER_APP_HOST, ident); 185 rsx! { 186 a { href: "{href}", class: "{class}", {props.children} } 187 } 188 } 189 AppLinkTarget::Invites { ident } => { 190 let href = format!("{}/{}/invites", WEAVER_APP_HOST, ident); 191 rsx! { 192 a { href: "{href}", class: "{class}", {props.children} } 193 } 194 } 195 } 196 } 197 LinkMode::CustomDomain => { 198 // Custom domain mode - uses CustomDomainRoute for path-based routing 199 match props.to.clone() { 200 AppLinkTarget::Entry { entry_path, .. } => { 201 // Entry by title maps to path page 202 let route = CustomDomainRoute::PathPage { 203 segments: vec![entry_path.to_string()], 204 }; 205 rsx! { 206 Link { to: route, class: "{class}", {props.children} } 207 } 208 } 209 AppLinkTarget::EntryByRkey { rkey, .. } => { 210 let route = CustomDomainRoute::EntryByRkey { rkey }; 211 rsx! { 212 Link { to: route, class: "{class}", {props.children} } 213 } 214 } 215 AppLinkTarget::EntryEdit { rkey, .. } => { 216 let route = CustomDomainRoute::EntryEdit { rkey }; 217 rsx! { 218 Link { to: route, class: "{class}", {props.children} } 219 } 220 } 221 AppLinkTarget::Notebook { .. } => { 222 let route = CustomDomainRoute::Root {}; 223 rsx! { 224 Link { to: route, class: "{class}", {props.children} } 225 } 226 } 227 AppLinkTarget::Profile { ident } => { 228 let route = CustomDomainRoute::Profile { ident }; 229 rsx! { 230 Link { to: route, class: "{class}", {props.children} } 231 } 232 } 233 // These go to main domain in custom domain mode 234 AppLinkTarget::StandaloneEntry { ident, rkey } => { 235 let href = format!("{}/{}/e/{}", WEAVER_APP_HOST, ident, rkey); 236 rsx! { 237 a { href: "{href}", class: "{class}", {props.children} } 238 } 239 } 240 AppLinkTarget::StandaloneEntryEdit { ident, rkey } => { 241 let href = format!("{}/{}/e/{}/edit", WEAVER_APP_HOST, ident, rkey); 242 rsx! { 243 a { href: "{href}", class: "{class}", {props.children} } 244 } 245 } 246 AppLinkTarget::NewDraft { ident, notebook } => { 247 let href = match notebook { 248 Some(nb) => format!("{}/{}/new?notebook={}", WEAVER_APP_HOST, ident, nb), 249 None => format!("{}/{}/new", WEAVER_APP_HOST, ident), 250 }; 251 rsx! { 252 a { href: "{href}", class: "{class}", {props.children} } 253 } 254 } 255 AppLinkTarget::Drafts { ident } => { 256 let href = format!("{}/{}/drafts", WEAVER_APP_HOST, ident); 257 rsx! { 258 a { href: "{href}", class: "{class}", {props.children} } 259 } 260 } 261 AppLinkTarget::Invites { ident } => { 262 let href = format!("{}/{}/invites", WEAVER_APP_HOST, ident); 263 rsx! { 264 a { href: "{href}", class: "{class}", {props.children} } 265 } 266 } 267 } 268 } 269 } 270} 271 272/// Navigation function type for programmatic routing. 273pub type NavigateFn = std::rc::Rc<dyn Fn(AppLinkTarget)>; 274 275/// Hook to get the app-wide navigation function. 276/// Must be used with AppNavigatorProvider in context. 277pub fn use_app_navigate() -> NavigateFn { 278 use_context::<NavigateFn>() 279} 280 281/// Provides the main domain navigation function. 282/// Call this in App to set up navigation context. 283pub fn use_main_navigator_provider() { 284 let navigator = use_navigator(); 285 use_context_provider(move || { 286 let navigator = navigator.clone(); 287 std::rc::Rc::new(move |target: AppLinkTarget| { 288 let route = match target { 289 AppLinkTarget::Entry { 290 ident, 291 book_title, 292 entry_path, 293 } => Route::EntryPage { 294 ident, 295 book_title, 296 title: entry_path, 297 }, 298 AppLinkTarget::EntryByRkey { 299 ident, 300 book_title, 301 rkey, 302 } => Route::NotebookEntryByRkey { 303 ident, 304 book_title, 305 rkey, 306 }, 307 AppLinkTarget::EntryEdit { 308 ident, 309 book_title, 310 rkey, 311 } => Route::NotebookEntryEdit { 312 ident, 313 book_title, 314 rkey, 315 }, 316 AppLinkTarget::Notebook { ident, book_title } => { 317 Route::NotebookIndex { ident, book_title } 318 } 319 AppLinkTarget::Profile { ident } => Route::RepositoryIndex { ident }, 320 AppLinkTarget::StandaloneEntry { ident, rkey } => { 321 Route::StandaloneEntry { ident, rkey } 322 } 323 AppLinkTarget::StandaloneEntryEdit { ident, rkey } => { 324 Route::StandaloneEntryEdit { ident, rkey } 325 } 326 AppLinkTarget::NewDraft { ident, notebook } => Route::NewDraft { ident, notebook }, 327 AppLinkTarget::Drafts { ident } => Route::DraftsList { ident }, 328 AppLinkTarget::Invites { ident } => Route::InvitesPage { ident }, 329 }; 330 navigator.push(route); 331 }) as NavigateFn 332 }); 333} 334 335/// Provides the subdomain navigation function. 336/// Call this in SubdomainApp to set up navigation context. 337pub fn use_subdomain_navigator_provider() { 338 let navigator = use_navigator(); 339 use_context_provider(move || { 340 let navigator = navigator.clone(); 341 std::rc::Rc::new(move |target: AppLinkTarget| { 342 match target { 343 // These navigate within subdomain 344 AppLinkTarget::Entry { entry_path, .. } => { 345 navigator.push(SubdomainRoute::SubdomainEntry { title: entry_path }); 346 } 347 AppLinkTarget::EntryByRkey { rkey, .. } => { 348 navigator.push(SubdomainRoute::SubdomainEntryByRkey { rkey }); 349 } 350 AppLinkTarget::EntryEdit { rkey, .. } => { 351 navigator.push(SubdomainRoute::SubdomainEntryEdit { rkey }); 352 } 353 AppLinkTarget::Notebook { .. } => { 354 navigator.push(SubdomainRoute::SubdomainLanding {}); 355 } 356 AppLinkTarget::Profile { ident } => { 357 navigator.push(SubdomainRoute::SubdomainProfile { ident }); 358 } 359 // These go to main domain - use window.location 360 AppLinkTarget::StandaloneEntry { ident, rkey } 361 | AppLinkTarget::StandaloneEntryEdit { ident, rkey } => { 362 #[cfg(target_arch = "wasm32")] 363 if let Some(window) = web_sys::window() { 364 let path = format!("{}/{}/e/{}", WEAVER_APP_HOST, ident, rkey); 365 let _ = window.location().set_href(&path); 366 } 367 #[cfg(not(target_arch = "wasm32"))] 368 { 369 let _ = ident; 370 let _ = rkey; 371 } 372 } 373 AppLinkTarget::NewDraft { ident, notebook } => { 374 #[cfg(target_arch = "wasm32")] 375 if let Some(window) = web_sys::window() { 376 let path = match notebook { 377 Some(nb) => { 378 format!("{}/{}/new?notebook={}", WEAVER_APP_HOST, ident, nb) 379 } 380 None => format!("{}/{}/new", WEAVER_APP_HOST, ident), 381 }; 382 let _ = window.location().set_href(&path); 383 } 384 #[cfg(not(target_arch = "wasm32"))] 385 { 386 let _ = notebook; 387 let _ = ident; 388 } 389 } 390 AppLinkTarget::Drafts { ident } => { 391 #[cfg(target_arch = "wasm32")] 392 if let Some(window) = web_sys::window() { 393 let path = format!("{}/{}/drafts", WEAVER_APP_HOST, ident); 394 let _ = window.location().set_href(&path); 395 } 396 #[cfg(not(target_arch = "wasm32"))] 397 let _ = ident; 398 } 399 AppLinkTarget::Invites { ident } => { 400 #[cfg(target_arch = "wasm32")] 401 if let Some(window) = web_sys::window() { 402 let path = format!("{}/{}/invites", WEAVER_APP_HOST, ident); 403 let _ = window.location().set_href(&path); 404 } 405 #[cfg(not(target_arch = "wasm32"))] 406 let _ = ident; 407 } 408 } 409 }) as NavigateFn 410 }); 411}