yggdrasil/pages/
post_detail.rs1use leptos::prelude::*;
21use leptos_meta::Title;
22use leptos_router::hooks::use_params_map;
23use leptos_router::{lazy_route, LazyRoute};
24
25use crate::api::posts::{get_public_post_by_slug, PublicPostResponse};
26use crate::components::comments::section::CommentSection;
27use crate::components::post::post_content::{
28 hydration_skeleton, PostContent, HYDRATION_PLACEHOLDER,
29};
30use crate::components::post::post_cover::PostCover;
31use crate::components::post::post_footer::PostFooter;
32use crate::components::post::post_header::PostHeader;
33use crate::components::post::post_toc::PostToc;
34use crate::components::share_meta::PostShareMeta;
35use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
36use crate::components::skeletons::post_detail_skeleton::PostDetailSkeleton;
37use crate::hooks::ssr_resource::{prefetch_route_data, route_data, PageJson, RouteData};
38use crate::models::post::{Post, PostFooterData, PostHeaderData, PostStatus};
39use crate::models::public_post::PublicPost;
40use crate::pages::not_found::{NotFound, PageLoadFailed};
41use crate::utils::error::is_not_found;
42
43pub struct PostRoute {
45 post: RouteData<PublicPostResponse>,
46}
47
48#[leptos::lazy]
50async fn fetch_post(slug: String) -> PageJson<PublicPostResponse> {
51 PageJson::with_hydration(get_public_post_by_slug(slug).await, hydration_form)
52}
53
54fn hydration_form(response: &PublicPostResponse) -> PublicPostResponse {
57 PublicPostResponse {
58 post: response.post.as_ref().map(|post| PublicPost {
59 id: post.id,
60 author_id: post.author_id,
61 title: post.title.clone(),
62 slug: post.slug.clone(),
63 summary: post.summary.clone(),
64 content_html: post.content_html.as_deref().map(hydration_skeleton),
65 published_at: post.published_at,
66 created_at: post.created_at,
67 updated_at: post.updated_at,
68 tags: post.tags.clone(),
69 cover_image: post.cover_image.clone(),
70 reading_time: post.reading_time,
71 word_count: post.word_count,
72 toc_html: post.toc_html.as_ref().map(|toc| {
73 if toc.is_empty() {
74 String::new()
75 } else {
76 HYDRATION_PLACEHOLDER.to_owned()
77 }
78 }),
79 prev_post: post.prev_post.clone(),
80 next_post: post.next_post.clone(),
81 }),
82 }
83}
84
85#[lazy_route]
86impl LazyRoute for PostRoute {
87 fn data() -> Self {
88 let params = use_params_map();
89 let slug = Memo::new(move |_| params.read().get("slug").unwrap_or_default());
90 Self {
91 post: route_data(move || slug.get(), fetch_post),
92 }
93 }
94
95 fn view(this: Self) -> AnyView {
98 let post = this.post;
99 view! {
100 <Suspense fallback=|| {
101 view! {
102 <DelayedSkeleton>
103 <PostDetailSkeleton/>
104 </DelayedSkeleton>
105 }
106 }>
107 {move || Suspend::new(async move {
108 match post.await {
109 Ok(PublicPostResponse { post: Some(post) }) => {
110 view! { <PostDetail post=post.into_display_post()/> }.into_any()
111 }
112 Ok(PublicPostResponse { post: None }) => view! { <NotFound/> }.into_any(),
113 Err(err) if is_not_found(&err) => view! { <NotFound/> }.into_any(),
114 Err(_) => view! { <PageLoadFailed/> }.into_any(),
115 }
116 })}
117 </Suspense>
118 }
119 .into_any()
120 }
121}
122
123impl PostRoute {
124 pub fn prefetch_data(slug: String) {
126 prefetch_route_data(slug, fetch_post);
127 }
128}
129
130#[component]
132fn PostDetail(post: Post) -> impl IntoView {
133 let header = PostHeaderData::from(&post);
134 let footer = PostFooterData::from(&post);
135 let cover = post
136 .cover_image
137 .clone()
138 .map(|cover| view! { <PostCover src=cover post_id=post.id/> });
139 let comments = (post.status == PostStatus::Published).then(|| {
141 let post_id = post.id;
142 view! {
143 <div class="post-comments">
144 <CommentSection post_id=post_id/>
145 </div>
146 }
147 });
148 let toc = post
149 .toc_html
150 .clone()
151 .filter(|t| !t.is_empty())
152 .map(|toc| view! { <PostToc toc_html=toc/> });
153 let content_html = post.content_html.clone().unwrap_or_default();
154 let title = format!("{} · Yggdrasil", post.title);
155 let vt_detail = post.id.to_string();
156
157 view! {
158 <div class="post-layout">
159 <article class="post-single animate-page-enter" data-vt-detail=vt_detail>
160 <PostHeader post=header/>
162 {cover}
163 <PostContent content_html=content_html/>
164 <PostFooter post=footer/>
165 {comments}
166 </article>
167 {toc}
168 </div>
169 <Title text=title/>
170 <PostShareMeta post=post/>
171 }
172}