at main 176 lines 7.1 kB view raw
1use crate::{ 2 Route, 3 auth::AuthState, 4 components::{EntryCard, NotebookCover, NotebookCss}, 5 components::button::{Button, ButtonVariant}, 6 data, 7}; 8use dioxus::prelude::*; 9use jacquard::{ 10 smol_str::{SmolStr, ToSmolStr, format_smolstr}, 11 types::ident::AtIdentifier, 12}; 13 14/// OpenGraph and Twitter Card meta tags for notebook index pages 15#[component] 16pub fn NotebookOgMeta( 17 title: String, 18 description: String, 19 image_url: String, 20 canonical_url: String, 21 author_handle: String, 22 entry_count: usize, 23) -> Element { 24 let page_title = format!("{} | @{} | Weaver", title, author_handle); 25 let full_description = if entry_count > 0 { 26 format!("{} entries · {}", entry_count, description) 27 } else { 28 description.clone() 29 }; 30 31 rsx! { 32 document::Title { "{page_title}" } 33 document::Meta { property: "og:title", content: "{title}" } 34 document::Meta { property: "og:description", content: "{full_description}" } 35 document::Meta { property: "og:image", content: "{image_url}" } 36 document::Meta { property: "og:type", content: "website" } 37 document::Meta { property: "og:url", content: "{canonical_url}" } 38 document::Meta { property: "og:site_name", content: "Weaver" } 39 document::Meta { name: "twitter:card", content: "summary_large_image" } 40 document::Meta { name: "twitter:title", content: "{title}" } 41 document::Meta { name: "twitter:description", content: "{full_description}" } 42 document::Meta { name: "twitter:image", content: "{image_url}" } 43 document::Meta { name: "twitter:creator", content: "@{author_handle}" } 44 } 45} 46 47// Card styles loaded at navbar level 48const LAYOUTS_CSS: Asset = asset!("/assets/styling/layouts.css"); 49 50/// The Blog page component that will be rendered when the current route is `[Route::Blog]` 51/// 52/// The component takes a `id` prop of type `i32` from the route enum. Whenever the id changes, the component function will be 53/// re-run and the rendered HTML will be updated. 54#[component] 55pub fn Notebook(ident: ReadSignal<AtIdentifier<'static>>, book_title: SmolStr) -> Element { 56 tracing::debug!( 57 "Notebook component rendering for ident: {:?}, book: {}", 58 ident(), 59 book_title 60 ); 61 rsx! { 62 NotebookCss { ident: ident.to_smolstr(), notebook: book_title } 63 Outlet::<Route> {} 64 } 65} 66 67#[component] 68pub fn NotebookIndex( 69 ident: ReadSignal<AtIdentifier<'static>>, 70 book_title: ReadSignal<SmolStr>, 71) -> Element { 72 tracing::info!( 73 "NotebookIndex: start, ident={:?}, book={}", 74 ident(), 75 book_title() 76 ); 77 // Fetch full notebook metadata with SSR support 78 // IMPORTANT: Call ALL hooks before any ? early returns to maintain hook order 79 let (notebook_result, notebook_data) = data::use_notebook(ident, book_title); 80 tracing::info!("NotebookIndex: use_notebook returned"); 81 let (entries_result, entries_resource) = data::use_notebook_entries(ident, book_title); 82 tracing::info!("NotebookIndex: use_notebook_entries returned"); 83 84 #[cfg(feature = "fullstack-server")] 85 notebook_result?; 86 tracing::info!("NotebookIndex: past notebook_result?"); 87 88 #[cfg(feature = "fullstack-server")] 89 entries_result?; 90 tracing::info!("NotebookIndex: past entries_result?"); 91 92 // Check ownership for "Add Entry" button 93 let auth_state = use_context::<Signal<AuthState>>(); 94 let is_owner = { 95 let current_did = auth_state.read().did.clone(); 96 match (&current_did, ident()) { 97 (Some(did), AtIdentifier::Did(ref ident_did)) => *did == *ident_did, 98 _ => false, 99 } 100 }; 101 102 rsx! { 103 document::Link { rel: "stylesheet", href: LAYOUTS_CSS } 104 105 match (&*notebook_data.read(), &*entries_resource.read()) { 106 (Some(data), Some(entries)) => { 107 let (notebook_view, _) = data; 108 let author_count = notebook_view.authors.len(); 109 110 // Build OG metadata 111 let og_title = notebook_view.title 112 .as_ref() 113 .map(|t| t.as_ref().to_string()) 114 .unwrap_or_else(|| "Untitled Notebook".to_string()); 115 116 let og_author = { 117 use weaver_api::sh_weaver::actor::ProfileDataViewInner; 118 notebook_view.authors.first() 119 .map(|a| match &a.record.inner { 120 ProfileDataViewInner::ProfileView(p) => p.handle.as_ref().to_smolstr(), 121 ProfileDataViewInner::ProfileViewDetailed(p) => p.handle.as_ref().to_smolstr(), 122 ProfileDataViewInner::TangledProfileView(p) => p.handle.as_ref().to_smolstr(), 123 _ => "unknown".into(), 124 }) 125 .unwrap_or_else(|| "unknown".into()) 126 }; 127 128 // NotebookView doesn't expose description directly, use empty for now 129 let og_description = String::new(); 130 131 let base = if crate::env::WEAVER_APP_ENV == "dev" { 132 format_smolstr!("http://127.0.0.1:{}", crate::env::WEAVER_PORT) 133 } else { 134 SmolStr::new_static(crate::env::WEAVER_APP_HOST) 135 }; 136 let og_image_url = format_smolstr!("{}/og/notebook/{}/{}.png", base, ident(), book_title()); 137 let canonical_url = format_smolstr!("{}/{}/{}", base, ident(), book_title()); 138 139 rsx! { 140 NotebookOgMeta { 141 title: og_title, 142 description: og_description, 143 image_url: og_image_url.to_string(), 144 canonical_url: canonical_url.to_string(), 145 author_handle: og_author.to_string(), 146 entry_count: entries.len(), 147 } 148 div { class: "notebook-layout", 149 aside { class: "notebook-sidebar", 150 NotebookCover { 151 notebook: notebook_view.clone(), 152 title: book_title().to_string(), 153 is_owner, 154 ident: Some(ident()) 155 } 156 } 157 158 main { class: "notebook-main", 159 div { class: "entries-list", 160 for entry in entries { 161 EntryCard { 162 entry: entry.clone(), 163 book_title: book_title(), 164 author_count, 165 ident: ident(), 166 } 167 } 168 } 169 } 170 } 171 } 172 }, 173 _ => rsx! { div { class: "loading", "Loading..." } } 174 } 175 } 176}