at main 408 lines 15 kB view raw
1#![allow(non_snake_case)] 2 3use dioxus::prelude::*; 4use jacquard::smol_str::{SmolStr, format_smolstr}; 5use jacquard::types::string::AtIdentifier; 6use weaver_api::sh_weaver::notebook::AuthorListView; 7 8use crate::components::css::DefaultNotebookCss; 9use crate::components::{AuthorList, extract_author_info}; 10 11#[component] 12pub fn WhiteWindEntry( 13 ident: ReadSignal<AtIdentifier<'static>>, 14 rkey: ReadSignal<SmolStr>, 15) -> Element { 16 use crate::components::{ENTRY_CSS, EntryOgMeta, calculate_reading_stats, extract_preview}; 17 18 let (entry_res, entry_data) = crate::data::use_whitewind_entry_data(ident, rkey); 19 20 #[cfg(feature = "fullstack-server")] 21 let _entry_res = entry_res?; 22 23 match &*entry_data.read() { 24 Some(data) => { 25 let title = data 26 .entry 27 .title 28 .as_ref() 29 .map(|t| t.as_ref()) 30 .unwrap_or("Untitled"); 31 32 let subtitle = data.entry.subtitle.as_ref().map(|s| s.as_ref().to_string()); 33 34 let base = if crate::env::WEAVER_APP_ENV == "dev" { 35 format_smolstr!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 36 } else { 37 SmolStr::new_static(crate::env::WEAVER_APP_HOST) 38 }; 39 let canonical_url = format_smolstr!("{}/{}/w/{}", base, ident(), rkey()); 40 41 let author_info = extract_author_info(&data.profile.inner); 42 let author_handle = author_info 43 .as_ref() 44 .map(|a| a.handle.as_ref().into()) 45 .unwrap_or_else(|| SmolStr::new_static("unknown")); 46 47 let description = extract_preview(&data.entry.content, 160); 48 let content = data.entry.content.clone(); 49 let (word_count, reading_time_mins) = calculate_reading_stats(&content); 50 51 let author_list_view = AuthorListView::new() 52 .index(0) 53 .record(data.profile.clone()) 54 .build(); 55 56 let formatted_date = data 57 .entry 58 .created_at 59 .as_ref() 60 .map(|d| d.as_ref().format("%B %d, %Y").to_string()); 61 62 rsx! { 63 EntryOgMeta { 64 title: title.to_string(), 65 description: description.clone(), 66 image_url: String::new(), 67 canonical_url: canonical_url.to_string(), 68 author_handle: author_handle.to_string(), 69 } 70 document::Link { rel: "stylesheet", href: ENTRY_CSS } 71 DefaultNotebookCss {} 72 73 div { class: "entry-page", 74 div { class: "entry-content-main notebook-content", 75 header { class: "entry-metadata", 76 div { class: "entry-header-row", 77 h1 { class: "entry-title", "{title}" } 78 } 79 if let Some(ref sub) = subtitle { 80 p { class: "entry-subtitle", "{sub}" } 81 } 82 div { class: "entry-meta-info", 83 div { class: "entry-authors", 84 AuthorList { authors: vec![author_list_view] } 85 } 86 if let Some(ref date) = formatted_date { 87 div { class: "entry-date", 88 time { "{date}" } 89 } 90 } 91 div { class: "entry-meta-secondary", 92 div { class: "entry-reading-stats", 93 span { class: "word-count", "{word_count} words" } 94 span { class: "reading-time", "{reading_time_mins} min read" } 95 } 96 } 97 } 98 div { class: "entry-source", 99 a { 100 href: "https://whtwnd.com/{author_handle}/{rkey()}", 101 target: "_blank", 102 class: "source-badge", 103 "View on WhiteWind ↗" 104 } 105 } 106 } 107 WhiteWindMarkdown { content: content.to_string() } 108 } 109 } 110 } 111 } 112 None => rsx! { p { "Loading..." } }, 113 } 114} 115 116#[component] 117fn WhiteWindMarkdown(content: String) -> Element { 118 use markdown_weaver::Parser; 119 use weaver_renderer::atproto::ClientWriter; 120 121 let html = { 122 let parser = 123 Parser::new_ext(&content, weaver_renderer::default_md_options()).into_offset_iter(); 124 let mut html_buf = String::new(); 125 let _ = ClientWriter::<_, _, ()>::new(parser, &mut html_buf, &content).run(); 126 html_buf 127 }; 128 129 rsx! { 130 div { 131 class: "entry", 132 dangerous_inner_html: "{html}" 133 } 134 } 135} 136 137#[component] 138pub fn LeafletEntry( 139 ident: ReadSignal<AtIdentifier<'static>>, 140 rkey: ReadSignal<SmolStr>, 141) -> Element { 142 use crate::components::{ENTRY_CSS, EntryOgMeta}; 143 144 let (entry_res, entry_data) = crate::data::use_leaflet_document_data(ident, rkey); 145 146 #[cfg(feature = "fullstack-server")] 147 let _entry_res = entry_res?; 148 149 match &*entry_data.read() { 150 Some(data) => { 151 let title = data.document.title.as_ref(); 152 153 let base = if crate::env::WEAVER_APP_ENV == "dev" { 154 format_smolstr!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 155 } else { 156 SmolStr::new_static(crate::env::WEAVER_APP_HOST) 157 }; 158 let canonical_url = format_smolstr!("{}/{}/l/{}", base, ident(), rkey()); 159 160 let author_info = extract_author_info(&data.profile.inner); 161 let author_handle = author_info 162 .as_ref() 163 .map(|a| a.handle.as_ref().into()) 164 .unwrap_or_else(|| SmolStr::new_static("unknown")); 165 166 let author_list_view = AuthorListView::new() 167 .index(0) 168 .record(data.profile.clone()) 169 .build(); 170 171 rsx! { 172 EntryOgMeta { 173 title: title.to_string(), 174 description: String::new(), 175 image_url: String::new(), 176 canonical_url: canonical_url.to_string(), 177 author_handle: author_handle.to_string(), 178 } 179 document::Link { rel: "stylesheet", href: ENTRY_CSS } 180 DefaultNotebookCss {} 181 182 div { class: "entry-page", 183 div { class: "entry-content-main notebook-content", 184 header { class: "entry-metadata", 185 div { class: "entry-header-row", 186 h1 { class: "entry-title", "{title}" } 187 } 188 div { class: "entry-meta-info", 189 div { class: "entry-authors", 190 AuthorList { authors: vec![author_list_view] } 191 } 192 } 193 if let Some(ref base_path) = data.publication_base_path { 194 div { class: "entry-source", 195 a { 196 href: "https://{base_path}/{rkey()}", 197 target: "_blank", 198 class: "source-badge", 199 "View on Leaflet ↗" 200 } 201 } 202 } 203 } 204 if let Some(ref html) = data.rendered_html { 205 div { 206 class: "entry leaflet-document", 207 dangerous_inner_html: "{html}" 208 } 209 } else { 210 p { "Rendering..." } 211 } 212 } 213 } 214 } 215 } 216 None => rsx! { p { "Loading..." } }, 217 } 218} 219 220#[cfg(feature = "pckt")] 221#[component] 222pub fn PcktEntry(ident: ReadSignal<AtIdentifier<'static>>, rkey: ReadSignal<SmolStr>) -> Element { 223 use crate::components::{ENTRY_CSS, EntryOgMeta}; 224 225 let (entry_res, entry_data) = crate::data::use_pckt_document_data(ident, rkey); 226 227 #[cfg(feature = "fullstack-server")] 228 let _entry_res = entry_res?; 229 230 match &*entry_data.read() { 231 Some(data) => { 232 let title = data.document.title.as_ref(); 233 234 let base = if crate::env::WEAVER_APP_ENV == "dev" { 235 format_smolstr!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 236 } else { 237 SmolStr::new_static(crate::env::WEAVER_APP_HOST) 238 }; 239 let canonical_url = format_smolstr!("{}/{}/sd/{}", base, ident(), rkey()); 240 241 let author_info = extract_author_info(&data.profile.inner); 242 let author_handle = author_info 243 .as_ref() 244 .map(|a| a.handle.as_ref().into()) 245 .unwrap_or_else(|| SmolStr::new_static("unknown")); 246 247 let author_list_view = AuthorListView::new() 248 .index(0) 249 .record(data.profile.clone()) 250 .build(); 251 252 let description = data 253 .document 254 .description 255 .as_ref() 256 .map(|d| d.as_ref().to_string()) 257 .unwrap_or_default(); 258 259 let formatted_date = data 260 .document 261 .published_at 262 .as_ref() 263 .format("%B %d, %Y") 264 .to_string(); 265 266 // Build external URL from publication URL + path (or rkey) 267 let doc_path = data 268 .document 269 .path 270 .as_ref() 271 .map(|p| p.as_ref().to_string()) 272 .unwrap_or_else(|| rkey().to_string()); 273 274 rsx! { 275 EntryOgMeta { 276 title: title.to_string(), 277 description: description.clone(), 278 image_url: String::new(), 279 canonical_url: canonical_url.to_string(), 280 author_handle: author_handle.to_string(), 281 } 282 document::Link { rel: "stylesheet", href: ENTRY_CSS } 283 DefaultNotebookCss {} 284 285 div { class: "entry-page", 286 div { class: "entry-content-main notebook-content", 287 header { class: "entry-metadata", 288 div { class: "entry-header-row", 289 h1 { class: "entry-title", "{title}" } 290 } 291 div { class: "entry-meta-info", 292 div { class: "entry-authors", 293 AuthorList { authors: vec![author_list_view] } 294 } 295 div { class: "entry-date", 296 time { "{formatted_date}" } 297 } 298 } 299 if let Some(ref pub_url) = data.publication_url { 300 { 301 let pub_url = pub_url.trim_end_matches('/'); 302 rsx! { 303 div { class: "entry-source", 304 a { 305 href: "{pub_url}/{doc_path}", 306 target: "_blank", 307 class: "source-badge", 308 "View on Pckt ↗" 309 } 310 } 311 } 312 } 313 } 314 } 315 if let Some(ref html) = data.rendered_html { 316 div { 317 class: "entry pckt-document", 318 dangerous_inner_html: "{html}" 319 } 320 } else { 321 p { "Rendering..." } 322 } 323 } 324 } 325 } 326 } 327 None => rsx! { p { "Loading..." } }, 328 } 329} 330 331// ============================================================================= 332// NSID route wrappers (allow replacing at:// with https://host/) 333// ============================================================================= 334 335#[component] 336pub fn WhiteWindEntryNsid( 337 ident: ReadSignal<AtIdentifier<'static>>, 338 rkey: ReadSignal<SmolStr>, 339) -> Element { 340 rsx! { WhiteWindEntry { ident, rkey } } 341} 342 343#[component] 344pub fn LeafletEntryNsid( 345 ident: ReadSignal<AtIdentifier<'static>>, 346 rkey: ReadSignal<SmolStr>, 347) -> Element { 348 rsx! { LeafletEntry { ident, rkey } } 349} 350 351#[cfg(feature = "pckt")] 352#[component] 353pub fn PcktEntryNsid( 354 ident: ReadSignal<AtIdentifier<'static>>, 355 rkey: ReadSignal<SmolStr>, 356) -> Element { 357 rsx! { PcktEntry { ident, rkey } } 358} 359 360#[cfg(feature = "pckt")] 361#[component] 362pub fn PcktEntryBlogNsid( 363 ident: ReadSignal<AtIdentifier<'static>>, 364 rkey: ReadSignal<SmolStr>, 365) -> Element { 366 rsx! { PcktEntry { ident, rkey } } 367} 368 369// ============================================================================= 370// Stub redirects when pckt feature is disabled 371// ============================================================================= 372 373#[cfg(not(feature = "pckt"))] 374#[component] 375pub fn PcktEntry(ident: ReadSignal<AtIdentifier<'static>>, rkey: ReadSignal<SmolStr>) -> Element { 376 use crate::Route; 377 let nav = use_navigator(); 378 use_effect(move || { 379 nav.replace(Route::RecordPage { 380 uri: vec![ 381 "at:".into(), 382 "".into(), 383 ident().to_string(), 384 "site.standard.document".into(), 385 rkey().to_string(), 386 ], 387 }); 388 }); 389 rsx! {} 390} 391 392#[cfg(not(feature = "pckt"))] 393#[component] 394pub fn PcktEntryNsid( 395 ident: ReadSignal<AtIdentifier<'static>>, 396 rkey: ReadSignal<SmolStr>, 397) -> Element { 398 rsx! { PcktEntry { ident, rkey } } 399} 400 401#[cfg(not(feature = "pckt"))] 402#[component] 403pub fn PcktEntryBlogNsid( 404 ident: ReadSignal<AtIdentifier<'static>>, 405 rkey: ReadSignal<SmolStr>, 406) -> Element { 407 rsx! { PcktEntry { ident, rkey } } 408}