1use dioxus::prelude::*;
5
6use crate::api::posts::{list_published_posts, PostListResponse};
7use crate::components::empty_state::EmptyState;
8use crate::components::post_card::PostCard;
9use crate::components::skeletons::delayed_skeleton::DelayedSkeleton;
10use crate::components::skeletons::home_skeleton::HomePostsSkeleton;
11use crate::components::ui::Pagination;
12use crate::router::Route;
13
14const POSTS_PER_PAGE: i32 = 10;
15
16#[component]
17pub fn Home() -> Element {
18 rsx! { HomePage { page: 1 } }
19}
20
21#[component]
22pub fn HomePage(page: i32) -> Element {
23 let current_page = page.max(1);
24 rsx! {
25 div { class: "home-page",
26 HomeIntro { current_page }
27 section { id: "home-posts", class: "home-posts", tabindex: "-1", aria_label: "文章列表",
28 SuspenseBoundary {
29 fallback: move |_| rsx! { DelayedSkeleton { HomePostsSkeleton { current_page } } },
30 HomePosts { key: "page-{current_page}", current_page }
32 }
33 }
34 footer { class: "home-colophon",
35 span { class: "home-colophon-mark", aria_hidden: "true", "✳" }
36 p { "文字落下的地方,便有了生长。" }
37 Link { class: "home-text-link", to: Route::Friends {},
38 "去朋友的花园坐坐" span { class: "home-arrow", aria_hidden: "true", "↗" }
39 }
40 }
41 }
42 }
43}
44
45#[component]
47pub(crate) fn HomeIntro(current_page: i32) -> Element {
48 if current_page > 1 {
49 return rsx! {
50 header { class: "home-page-heading home-enter",
51 Link { class: "home-text-link", to: Route::Home {},
52 span { class: "home-arrow", aria_hidden: "true", "←" } "回到首页"
53 }
54 p { class: "home-eyebrow", "THE JOURNAL / 过往篇章" }
55 h1 { "往前翻," span { "还有故事。" } }
56 p { class: "home-description", "第 {current_page} 页 · 沿着文字,走进更早的年轮。" }
57 }
58 };
59 }
60
61 rsx! {
62 header { class: "home-hero",
63 div { class: "home-hero-copy home-enter",
64 p { class: "home-eyebrow", span { aria_hidden: "true" } "YGGDRASIL / 一隅数字花园" }
65 h1 { "让想法生根," span { "让文字成林。" } }
66 p { class: "home-description",
67 "记录技术、生活,与偶然闪光的念头。"
68 br {}
69 "在这里,慢慢写,也慢慢生长。"
70 }
71 div { class: "home-hero-actions",
72 a { class: "home-reading-link", href: "#home-posts",
73 "开始阅读" span { class: "home-arrow", aria_hidden: "true", "↓" }
74 }
75 Link { class: "home-text-link", to: Route::About {},
76 "关于这里" span { class: "home-arrow", aria_hidden: "true", "↗" }
77 }
78 }
79 }
80 Link { class: "home-garden home-enter", to: Route::About {}, aria_label: "认识这棵世界树",
81 svg { class: "home-rings", view_box: "0 0 360 360", fill: "none", "aria-hidden": "true",
82 g { class: "home-ring-lines", stroke: "currentColor", stroke_width: "0.8",
83 for i in 0..9 {
84 ellipse {
85 key: "{i}", cx: "180", cy: "180",
86 rx: "{42 + i * 14}", ry: "{38 + i * 13}",
87 transform: "rotate({i * 19} 180 180)",
88 }
89 }
90 }
91 circle { cx: "180", cy: "180", r: "169", stroke: "currentColor", stroke_dasharray: "1 9", opacity: "0.3" }
92 path { d: "M180 6V28M180 332V354M6 180H28M332 180H354", stroke: "currentColor", opacity: "0.4" }
93 g { class: "home-sprout", stroke: "currentColor", stroke_width: "1.8", stroke_linecap: "round", stroke_linejoin: "round",
94 path { d: "M180 207V175M180 190C157 190 148 173 151 156C169 156 183 166 180 190Z" }
95 path { d: "M180 176C180 156 192 147 210 149C210 165 199 177 180 176ZM180 190L163 170M180 176L198 159M166 208H194" }
96 }
97 g { class: "home-ring-orbit", fill: "currentColor",
98 circle { cx: "180", cy: "37", r: "4" }
99 circle { cx: "73", cy: "272", r: "2.5", opacity: "0.6" }
100 }
101 }
102 span { class: "home-garden-caption", "每一篇,都是新的年轮" span { class: "home-arrow", aria_hidden: "true", "↗" } }
103 }
104 }
105 }
106}
107
108#[component]
109fn HomePosts(current_page: i32) -> Element {
110 let router = dioxus::router::router();
111 let requested_page = use_memo(move || match router.current::<Route>() {
113 Route::HomePage { page } => page.max(1),
114 _ => current_page,
115 });
116 let mut posts_res =
117 use_server_future(move || list_published_posts(requested_page(), POSTS_PER_PAGE))?;
118
119 let posts_data = posts_res.read();
120 match posts_data.as_ref() {
121 Some(Ok(PostListResponse { posts, total })) => {
122 let total = *total;
123 rsx! {
124 span { hidden: true, "data-vt-list": "true" }
125 HomePostsHeading { current_page, total }
126 if !posts.is_empty() {
127 div { key: "page-{current_page}", class: "home-post-list",
128 for (index, post) in posts.iter().enumerate() {
129 div {
130 key: "{post.id}",
131 class: if current_page == 1 && index == 0 { "home-entry home-entry-featured home-enter" } else { "home-entry home-enter" },
132 style: "--home-delay: {index.min(5) * 45}ms",
133 if current_page == 1 && index == 0 {
134 span { class: "home-featured-label", "最新一篇" span { " / LATEST ENTRY" } }
135 } else {
136 span { class: "home-entry-number", aria_hidden: "true",
137 {format!("{:02}", (i64::from(current_page) - 1) * i64::from(POSTS_PER_PAGE) + index as i64 + 1)}
138 }
139 }
140 PostCard { post: post.clone(), compact: true }
141 span { class: "home-entry-arrow home-arrow", aria_hidden: "true", "↗" }
142 }
143 }
144 }
145 Pagination {
146 variant: "frontend", current_page, total, per_page: POSTS_PER_PAGE,
147 prev_route: if current_page - 1 <= 1 { Route::Home {} } else { Route::HomePage { page: current_page - 1 } },
148 next_route: Route::HomePage { page: current_page + 1 },
149 unit: "篇",
150 }
151 } else if total == 0 {
152 EmptyState { title: "还没有文章", description: "这片花园,正等待第一颗文字的种子。" }
153 } else {
154 div { class: "home-status",
155 EmptyState { title: "已经翻到最后了", description: "这一页还没有文字,回到首页看看最近的记录吧。" }
156 Link { class: "home-reading-link", to: Route::Home {}, "回到首页" span { class: "home-arrow", aria_hidden: "true", "↗" } }
157 }
158 }
159 }
160 }
161 Some(Err(_)) => rsx! {
162 span { hidden: true, "data-vt-list": "true" }
163 HomePostsHeading { current_page }
164 HomePostsError {
165 current_page,
166 on_recovered: move |response| posts_res.set(Some(Ok(response))),
167 }
168 },
169 _ => rsx! { DelayedSkeleton { HomePostsSkeleton { current_page } } },
170 }
171}
172
173#[component]
174fn HomePostsHeading(current_page: i32, total: Option<i64>) -> Element {
175 rsx! {
176 div { class: "home-section-heading home-enter",
177 div { class: "home-section-title",
178 h2 { if current_page == 1 { "最近写下" } else { "过往篇章" } }
179 if let Some(total) = total {
180 span { class: "home-post-count", "共 {total} 篇" }
181 }
182 }
183 Link { class: "home-text-link", to: Route::Archives {},
184 "全部归档" span { class: "home-arrow", aria_hidden: "true", "↗" }
185 }
186 }
187 }
188}
189
190#[component]
192fn HomePostsError(current_page: i32, on_recovered: EventHandler<PostListResponse>) -> Element {
193 let router = dioxus::router::router();
194 let mut retrying = use_signal(|| false);
195 let mut retry_failed = use_signal(|| false);
196
197 rsx! {
198 div { class: "home-error", role: "group", aria_labelledby: "home-error-title",
199 div { class: "home-error-copy",
200 p { class: "home-error-eyebrow",
201 span { class: "home-error-pause", aria_hidden: "true", "Ⅱ" }
202 "稍作停留" span { class: "home-error-eyebrow-en", " / A LITTLE PAUSE" }
203 }
204 h3 { id: "home-error-title", "文章暂时无法加载" }
205 p { class: "home-error-description", role: "status", aria_live: "polite", aria_atomic: "true",
206 if retry_failed() {
207 "这次仍未加载成功,稍后再试一次吧。"
208 } else {
209 "加载遇到了一点阻碍,稍后再试一次吧。"
210 }
211 if retrying() {
212 span { class: "sr-only", "正在重新加载文章,请稍候。" }
213 }
214 }
215 button {
216 class: "home-error-retry",
217 r#type: "button",
218 aria_disabled: retrying(),
219 aria_busy: retrying(),
220 onclick: move |_| {
221 if retrying() {
222 return;
223 }
224 retrying.set(true);
225 retry_failed.set(false);
226 let request_route = router.current::<Route>();
227 spawn(async move {
228 let result = list_published_posts(current_page, POSTS_PER_PAGE).await;
229 if router.current::<Route>() != request_route {
231 return;
232 }
233 retrying.set(false);
234 match result {
235 Ok(response) => on_recovered.call(response),
236 Err(_) => retry_failed.set(true),
237 }
238 });
239 },
240 svg {
241 class: "home-error-retry-icon", view_box: "0 0 24 24", fill: "none",
242 stroke: "currentColor", stroke_width: "1.6", stroke_linecap: "round", stroke_linejoin: "round",
243 "aria-hidden": "true", "focusable": "false",
244 path { d: "M20 7V3M20 7H16M20 7A8 8 0 1 0 20 16" }
245 }
246 if retrying() { "正在重新加载…" } else { "重新加载" }
247 }
248 }
249 svg {
250 class: "home-error-art", view_box: "0 0 240 240", fill: "none",
251 "aria-hidden": "true", "focusable": "false",
252 g { class: "home-error-rings", stroke: "currentColor", stroke_width: "0.8", stroke_linecap: "round",
253 circle { cx: "120", cy: "112", r: "94", stroke_dasharray: "370 52 72 97", transform: "rotate(-62 120 112)" }
254 circle { cx: "120", cy: "112", r: "78", stroke_dasharray: "264 42 65 119", transform: "rotate(-28 120 112)" }
255 circle { cx: "120", cy: "112", r: "62", stroke_dasharray: "198 36 59 97", transform: "rotate(-78 120 112)" }
256 circle { cx: "120", cy: "112", r: "108", stroke_dasharray: "1 11", opacity: "0.55" }
257 }
258 g { stroke: "currentColor", stroke_linecap: "round", stroke_linejoin: "round",
259 path { class: "home-error-book-fill", stroke_width: "1.5", d: "M120 161C101 148 79 146 58 150L58 184C82 180 102 185 120 194C138 185 158 180 182 184V150C161 146 139 148 120 161Z" }
260 path { stroke_width: "1.5", d: "M120 162V194M53 156L50 190C76 186 100 192 120 201C140 192 164 186 190 190L187 156" }
261 path { opacity: "0.35", d: "M70 161C83 160 96 163 108 168M70 170C83 169 96 172 108 177M132 168C144 163 157 160 170 161M132 177C144 172 157 169 170 170" }
262 path { stroke_width: "1.8", d: "M120 161V105" }
263 path { class: "home-error-leaf", stroke_width: "1.5", d: "M120 131C96 132 81 115 83 93C104 93 121 106 120 131ZM120 113C118 89 131 73 154 72C157 94 143 113 120 113Z" }
264 path { stroke_width: "1.2", d: "M120 132L95 106M120 113L142 86" }
265 path { opacity: "0.5", d: "M52 72V82M47 77H57M182 102V110M178 106H186M162 38V44M159 41H165" }
266 }
267 g { fill: "currentColor",
268 circle { cx: "70", cy: "36", r: "2.5", opacity: "0.65" }
269 circle { cx: "209", cy: "140", r: "2", opacity: "0.5" }
270 circle { cx: "31", cy: "123", r: "1.5", opacity: "0.4" }
271 }
272 path { d: "M104 219H136", stroke: "currentColor", stroke_linecap: "round", opacity: "0.35" }
273 }
274 }
275 }
276}