yggdrasil/pages/components_showcase/
layout_article_previews.rs1use leptos::prelude::*;
4
5use crate::components::post::post_content::PostContent;
6use crate::components::post::post_footer::PostFooter;
7use crate::components::post::post_header::PostHeader;
8use crate::components::post::post_toc::PostToc;
9use crate::models::post::{PostFooterData, PostHeaderData, PostNav, PostStatus};
10
11pub(super) fn preview(slug: &str, detail: bool) -> Option<AnyView> {
12 match slug {
13 "post-header" => Some(view! { <PostHeaderPreview detail/> }.into_any()),
14 "post-content" => Some(view! { <PostContentPreview detail/> }.into_any()),
15 "post-footer" => Some(view! { <PostFooterPreview detail/> }.into_any()),
16 "post-toc" => Some(view! { <PostTocPreview detail/> }.into_any()),
17 _ => None,
18 }
19}
20
21#[component]
22fn PostHeaderPreview(detail: bool) -> impl IntoView {
23 let mut post = super::sample_post();
24 post.summary = Some(
25 "在时间的缝隙里,种下一些文字。记录设计、代码与日常,让零散的念头慢慢长成一片风景。"
26 .to_string(),
27 );
28 let mut draft = post.clone();
29 draft.title = "还在生长的想法".to_string();
30 draft.summary = Some("这一页,留给尚未完成的念头。".to_string());
31 draft.status = PostStatus::Draft;
32 draft.published_at = None;
33 let mut without_summary = post.clone();
34 without_summary.title = "在时间的缝隙里种下一些文字:从零开始构建自己的数字花园".to_string();
35 without_summary.summary = None;
36 without_summary.tags = Vec::new();
37
38 let examples = detail.then(|| {
39 view! {
40 <div class="showcase-post-header-examples">
41 <section class="showcase-post-header-example">
42 <h3>"草稿" <span>"标题旁显示草稿标记"</span></h3>
43 <article
44 class="showcase-post-header-sheet"
45 data-showcase-sample="draft"
46 inert="true"
47 >
48 <PostHeader post=PostHeaderData::from(&draft)/>
49 </article>
50 </section>
51 <section class="showcase-post-header-example">
52 <h3>"长标题 · 无摘要 · 无标签" <span>"面包屑只剩「文章」"</span></h3>
53 <article
54 class="showcase-post-header-sheet"
55 data-showcase-sample="no-summary"
56 inert="true"
57 >
58 <PostHeader post=PostHeaderData::from(&without_summary)/>
59 </article>
60 </section>
61 </div>
62 }
63 });
64
65 view! {
66 <div class="showcase-post-header-demo" data-showcase-preview="post-header">
67 <article
68 class="showcase-post-header-sheet"
69 data-showcase-sample="published"
70 inert="true"
71 >
72 <PostHeader post=PostHeaderData::from(&post)/>
73 </article>
74 {examples}
75 </div>
76 }
77}
78
79fn article_html(prefix: &str, alternate: bool) -> String {
80 let opening = if alternate {
81 "雨停之后,树叶留下细小的光。"
82 } else {
83 "一片叶子,也记得来时的风。"
84 };
85 format!(
86 r#"<h2 id="{prefix}-opening">从一片叶子开始</h2>
87<p>{opening} 这段固定文章展示正文排版和图片,不连接任何服务。</p>
88<h3 id="{prefix}-details">沿途所见</h3>
89<ul><li>清晨的光</li><li>缓慢生长的枝条</li></ul>
90<blockquote><p>让想法,像树一样生长。</p></blockquote>
91<pre><code class="language-rust">let leaf = "Yggdrasil";
92println!("{{leaf}}");</code></pre>
93<h2 id="{prefix}-image">留下一张照片</h2>
94<p><img src="/images/samples/clay-circle.webp" alt="暖白纸面上的一轮陶土色圆" loading="lazy"></p>"#
95 )
96}
97
98fn toc_html(prefix: &str) -> String {
99 format!(
100 r##"<ul><li><a href="#{prefix}-opening">从一片叶子开始</a><ul><li><a href="#{prefix}-details">沿途所见</a></li></ul></li><li><a href="#{prefix}-image">留下一张照片</a></li></ul>"##
101 )
102}
103
104#[component]
105fn PostContentPreview(detail: bool) -> impl IntoView {
106 let alternate = RwSignal::new(false);
107 let (scope_id, heading_prefix) = if detail {
108 (
109 "showcase-post-content-detail",
110 "showcase-post-content-detail",
111 )
112 } else {
113 ("showcase-post-content-card", "showcase-post-content-card")
114 };
115 let toggle = detail.then(|| {
116 view! {
117 <button
118 class="mb-4 text-sm text-clay-text"
119 type="button"
120 on:click=move |_| alternate.update(|value| *value = !*value)
121 >
122 "切换文章样例"
123 </button>
124 }
125 });
126
127 view! {
128 <div class="showcase-article-window" data-showcase-preview="post-content">
129 {toggle}
130 {move || {
132 let variant = alternate.get();
133 view! {
134 <PostContent
135 content_html=article_html(heading_prefix, variant)
136 scope_id=scope_id
137 />
138 }
139 }}
140 </div>
141 }
142}
143
144#[component]
145fn PostFooterPreview(detail: bool) -> impl IntoView {
146 let prev = PostNav {
147 title: "沿着风的方向".to_string(),
148 slug: "sample-previous".to_string(),
149 };
150 let next = PostNav {
151 title: "下一圈年轮".to_string(),
152 slug: "sample-next".to_string(),
153 };
154 let footer = |prev_post: Option<PostNav>, next_post: Option<PostNav>| PostFooterData {
155 prev_post,
156 next_post,
157 };
158 let examples = detail.then(|| {
159 view! {
160 <div class="showcase-post-nav-examples">
161 <section class="showcase-post-nav-example" data-showcase-sample="next-only">
162 <h3>"第一篇文章" <span>"只有下一篇"</span></h3>
163 <PostFooter post=footer(None, Some(next.clone())) sample=true/>
164 </section>
165 <section class="showcase-post-nav-example" data-showcase-sample="prev-only">
166 <h3>"最后一篇文章" <span>"只有上一篇"</span></h3>
167 <PostFooter post=footer(Some(prev.clone()), None) sample=true/>
168 </section>
169 <section class="showcase-post-nav-empty" data-showcase-sample="empty">
170 <PostFooter post=footer(None, None) sample=true/>
171 <p>"没有相邻文章时,整个页脚不渲染。"</p>
172 </section>
173 </div>
174 <p class="showcase-post-footer-note">"试试悬停或用 Tab 聚焦卡片。样例链接停留在本页。"</p>
175 }
176 });
177
178 view! {
179 <div class="showcase-article-window" data-showcase-preview="post-footer">
180 <div data-showcase-sample="both">
181 <PostFooter post=footer(Some(prev.clone()), Some(next.clone())) sample=true/>
182 </div>
183 {examples}
184 </div>
185 }
186}
187
188#[component]
189fn PostTocPreview(detail: bool) -> impl IntoView {
190 let (scroll_id, nav_id, content_id) = if detail {
191 (
192 "showcase-post-toc-scroll-detail",
193 "showcase-post-toc-nav-detail",
194 "showcase-post-toc-content-detail",
195 )
196 } else {
197 (
198 "showcase-post-toc-scroll-card",
199 "showcase-post-toc-nav-card",
200 "showcase-post-toc-content-card",
201 )
202 };
203
204 view! {
205 <div
206 id=scroll_id
207 class="showcase-toc-window showcase-article-window"
208 data-showcase-preview="post-toc"
209 data-local-anchor-scroll="true"
210 >
211 <PostToc
212 toc_html=toc_html(content_id)
213 title="文章目录"
214 nav_id=nav_id
215 content_id=content_id
216 scroll_id=scroll_id
217 />
218 <PostContent content_html=article_html(content_id, false) scope_id=content_id/>
219 </div>
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn article_and_toc_share_scoped_headings() {
229 let html = article_html("sample", false);
230 let toc = toc_html("sample");
231 for id in ["sample-opening", "sample-details", "sample-image"] {
232 assert!(html.contains(&format!("id=\"{id}\"")));
233 assert!(toc.contains(&format!("href=\"#{id}\"")));
234 }
235 assert!(!html.contains("data-runnable"));
236 }
237
238 #[test]
239 fn alternate_article_changes_real_content() {
240 assert_ne!(article_html("sample", false), article_html("sample", true));
241 }
242}