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