at main 291 lines 12 kB view raw
1#![allow(non_snake_case)] 2 3use dioxus::prelude::*; 4use jacquard::smol_str::{SmolStr, ToSmolStr, format_smolstr}; 5use jacquard::types::string::AtIdentifier; 6 7use crate::components::NotebookCss; 8use crate::components::css::DefaultNotebookCss; 9 10/// View a standalone entry by rkey (not in notebook context). 11#[component] 12pub fn StandaloneEntry( 13 ident: ReadSignal<AtIdentifier<'static>>, 14 rkey: ReadSignal<SmolStr>, 15) -> Element { 16 use crate::components::{ 17 ENTRY_CSS, EntryMarkdown, EntryMetadata, EntryOgMeta, NavButton, calculate_reading_stats, extract_preview, 18 }; 19 use weaver_api::sh_weaver::actor::ProfileDataViewInner; 20 21 let (entry_res, entry_data) = crate::data::use_standalone_entry_data(ident, rkey); 22 23 #[cfg(feature = "fullstack-server")] 24 let _entry_res = entry_res?; 25 26 match &*entry_data.read() { 27 Some(data) => { 28 let entry_view = &data.entry_view; 29 let entry_record = &data.entry; 30 31 let title = entry_view 32 .title 33 .as_ref() 34 .map(|t| t.as_ref()) 35 .unwrap_or("Untitled"); 36 37 tracing::info!("Entry: {title}"); 38 let author_handle = entry_view 39 .authors 40 .first() 41 .map(|a| match &a.record.inner { 42 ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_smolstr(), 43 ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_smolstr(), 44 ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_smolstr(), 45 _ => "unknown".into(), 46 }) 47 .unwrap_or_else(|| "unknown".into()); 48 49 let base = if crate::env::WEAVER_APP_ENV == "dev" { 50 format_smolstr!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 51 } else { 52 SmolStr::new_static(crate::env::WEAVER_APP_HOST) 53 }; 54 let canonical_url = format_smolstr!("{}/{}/e/{}", base, ident(), rkey()); 55 let description = extract_preview(&entry_record.content, 160); 56 57 let entry_signal = use_signal(|| data.entry.clone()); 58 59 if let Some(ref ctx) = data.notebook_context { 60 let book_entry_view = &ctx.book_entry_view; 61 let notebook = &ctx.notebook; 62 let book_title: SmolStr = notebook 63 .title 64 .as_ref() 65 .map(|t| t.as_ref().into()) 66 .unwrap_or_else(|| "Untitled".into()); 67 68 rsx! { 69 EntryOgMeta { 70 title: title.to_string(), 71 description: description.clone(), 72 image_url: String::new(), 73 canonical_url: canonical_url.to_string(), 74 author_handle: author_handle.to_string(), 75 book_title: Some(book_title.to_string()), 76 } 77 document::Link { rel: "stylesheet", href: ENTRY_CSS } 78 NotebookCss { ident: ident().to_smolstr(), notebook: book_title.clone() } 79 80 div { class: "entry-page", 81 if let Some(ref prev) = book_entry_view.prev { 82 div { class: "nav-gutter nav-prev", 83 NavButton { 84 direction: "prev", 85 entry: prev.entry.clone(), 86 ident: ident(), 87 book_title: book_title.clone() 88 } 89 } 90 } 91 92 div { class: "entry-content-main notebook-content", 93 { 94 let (word_count, reading_time_mins) = calculate_reading_stats(&entry_record.content); 95 rsx! { 96 EntryMetadata { 97 entry_view: entry_view.clone(), 98 created_at: entry_record.created_at.clone(), 99 entry_uri: entry_view.uri.clone(), 100 book_title: Some(book_title.clone()), 101 ident: ident(), 102 word_count: Some(word_count), 103 reading_time_mins: Some(reading_time_mins) 104 } 105 } 106 } 107 EntryMarkdown { content: entry_signal, ident } 108 } 109 110 if let Some(ref next) = book_entry_view.next { 111 div { class: "nav-gutter nav-next", 112 NavButton { 113 direction: "next", 114 entry: next.entry.clone(), 115 ident: ident(), 116 book_title: book_title.clone() 117 } 118 } 119 } 120 } 121 } 122 } else { 123 // Standalone view without notebook navigation 124 rsx! { 125 EntryOgMeta { 126 title: title.to_string(), 127 description: description.clone(), 128 image_url: String::new(), 129 canonical_url: canonical_url.to_string(), 130 author_handle: author_handle.to_string(), 131 } 132 document::Link { rel: "stylesheet", href: ENTRY_CSS } 133 DefaultNotebookCss {} 134 135 136 div { class: "entry-page", 137 div { class: "entry-content-main notebook-content", 138 { 139 let (word_count, reading_time_mins) = calculate_reading_stats(&entry_record.content); 140 rsx! { 141 EntryMetadata { 142 entry_view: entry_view.clone(), 143 created_at: entry_record.created_at.clone(), 144 entry_uri: entry_view.uri.clone(), 145 book_title: None, 146 ident: ident(), 147 word_count: Some(word_count), 148 reading_time_mins: Some(reading_time_mins) 149 } 150 } 151 } 152 EntryMarkdown { content: entry_signal, ident } 153 } 154 } 155 } 156 } 157 } 158 None => rsx! { p { "Loading..." } }, 159 } 160} 161 162/// View a notebook entry by rkey. 163#[component] 164pub fn NotebookEntryByRkey( 165 ident: ReadSignal<AtIdentifier<'static>>, 166 book_title: ReadSignal<SmolStr>, 167 rkey: ReadSignal<SmolStr>, 168) -> Element { 169 use crate::components::{ 170 ENTRY_CSS, EntryMarkdown, EntryMetadata, EntryOgMeta, NavButton, calculate_reading_stats, extract_preview, 171 }; 172 use weaver_api::sh_weaver::actor::ProfileDataViewInner; 173 174 let (entry_res, entry_data) = crate::data::use_notebook_entry_by_rkey(ident, book_title, rkey); 175 176 #[cfg(feature = "fullstack-server")] 177 let _entry_res = entry_res?; 178 179 match &*entry_data.read() { 180 Some((book_entry_view, entry_record)) => { 181 let entry_view = &book_entry_view.entry; 182 183 let title = entry_view 184 .title 185 .as_ref() 186 .map(|t| t.as_ref()) 187 .unwrap_or("Untitled"); 188 189 let entry_path = entry_view 190 .path 191 .as_ref() 192 .map(|p| p.as_ref().to_smolstr()) 193 .unwrap_or_else(|| title.into()); 194 195 tracing::info!("Entry: {entry_path} - {title}"); 196 197 let author_handle = entry_view 198 .authors 199 .first() 200 .map(|a| match &a.record.inner { 201 ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_smolstr(), 202 ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_smolstr(), 203 ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_smolstr(), 204 _ => "unknown".into(), 205 }) 206 .unwrap_or_else(|| "unknown".into()); 207 208 let base = if crate::env::WEAVER_APP_ENV == "dev" { 209 format_smolstr!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 210 } else { 211 SmolStr::new_static(crate::env::WEAVER_APP_HOST) 212 }; 213 let canonical_url = format_smolstr!("{}/{}/{}/e/{}", base, ident(), book_title(), rkey()); 214 let og_image_url = format_smolstr!( 215 "{}/og/{}/{}/{}.png", 216 base, 217 ident(), 218 book_title(), 219 entry_path 220 ); 221 222 let description = extract_preview(&entry_record.content, 160); 223 let entry_signal = use_signal(|| entry_record.clone()); 224 225 rsx! { 226 EntryOgMeta { 227 title: title.to_string(), 228 description: description, 229 image_url: og_image_url.to_string(), 230 canonical_url: canonical_url.to_string(), 231 author_handle: author_handle.to_string(), 232 book_title: Some(book_title().to_string()), 233 } 234 document::Link { rel: "stylesheet", href: ENTRY_CSS } 235 NotebookCss { ident: ident().to_smolstr(), notebook: book_title() } 236 237 div { class: "entry-page", 238 if let Some(ref prev) = book_entry_view.prev { 239 div { class: "nav-gutter nav-prev", 240 NavButton { 241 direction: "prev", 242 entry: prev.entry.clone(), 243 ident: ident(), 244 book_title: book_title() 245 } 246 } 247 } 248 249 div { class: "entry-content-main notebook-content", 250 { 251 let (word_count, reading_time_mins) = calculate_reading_stats(&entry_record.content); 252 rsx! { 253 EntryMetadata { 254 entry_view: entry_view.clone(), 255 created_at: entry_record.created_at.clone(), 256 entry_uri: entry_view.uri.clone(), 257 book_title: Some(book_title()), 258 ident: ident(), 259 word_count: Some(word_count), 260 reading_time_mins: Some(reading_time_mins) 261 } 262 } 263 } 264 EntryMarkdown { content: entry_signal, ident } 265 } 266 267 if let Some(ref next) = book_entry_view.next { 268 div { class: "nav-gutter nav-next", 269 NavButton { 270 direction: "next", 271 entry: next.entry.clone(), 272 ident: ident(), 273 book_title: book_title() 274 } 275 } 276 } 277 } 278 } 279 } 280 None => rsx! { p { "Loading..." } }, 281 } 282} 283 284/// NSID route wrapper for StandaloneEntry (allows replacing at:// with https://host/) 285#[component] 286pub fn StandaloneEntryNsid( 287 ident: ReadSignal<AtIdentifier<'static>>, 288 rkey: ReadSignal<SmolStr>, 289) -> Element { 290 rsx! { StandaloneEntry { ident, rkey } } 291}