atproto blogging
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}