yggdrasil/pages/
not_found.rs1use leptos::prelude::*;
8use leptos::task::spawn_local;
9use leptos_meta::Title;
10use leptos_router::hooks::use_location;
11
12use crate::api::posts::{recommend_for_path, PathRecommendations};
13use crate::components::empty_state::{EmptyState, EmptyStateAction};
14use crate::components::mascot::Mascot;
15use crate::components::ui::{Icon, IconName};
16
17#[component]
24pub fn NotFound() -> impl IntoView {
25 crate::request_ctx::set_status(404);
26 let location = use_location();
27 let path = Memo::new(move |_| {
29 let pathname = location.pathname.get();
30 let search = location.search.get();
31 if search.is_empty() {
32 pathname
33 } else {
34 format!("{pathname}?{search}")
35 }
36 });
37
38 view! {
39 <Title text="页面不存在 · Yggdrasil"/>
40 <div class="not-found page-enter">
41 {Mascot::Panic.img("mascot not-found-mascot")}
42 <p class="not-found-code tabular-nums">"404"</p>
43 <h1 class="not-found-title">"这个页面不存在"</h1>
44 <p class="not-found-description">"地址可能写错了,或者页面已经移走。"</p>
45 <p class="not-found-path">
46 <code title=move || path.get()>{move || path.get()}</code>
47 </p>
48 <div class="not-found-actions">
49 <button
50 type="button"
51 class="btn btn-primary btn-lg"
52 on:click=move |_| crate::navigation::push("/")
53 >
54 "返回首页"
55 </button>
56 <a class="btn btn-secondary btn-lg" href="/search">
57 <Icon name=IconName::Search size=16/>
58 "搜索"
59 </a>
60 </div>
61 <Recommendations path=path/>
62 </div>
63 }
64}
65
66#[component]
70pub fn PageLoadFailed() -> impl IntoView {
71 crate::request_ctx::set_status(500);
72 view! {
73 <div class="flex min-h-[50vh] items-center justify-center px-6 md:min-h-[55vh] animate-page-enter">
74 <EmptyState
75 mascot=Mascot::Cry
76 title="页面加载失败"
77 description="网络或服务出了点问题,稍后再试一次。"
78 action=EmptyStateAction::new("返回首页", move |_| crate::navigation::push("/"))
79 />
80 </div>
81 }
82}
83
84#[component]
89fn Recommendations(#[prop(into)] path: Signal<String>) -> impl IntoView {
90 let result = RwSignal::new(None::<PathRecommendations>);
91 let generation = StoredValue::new(0u64);
92 Effect::new(move |_| {
93 let path = path.get();
94 let current = generation.get_value() + 1;
95 generation.set_value(current);
96 result.set(None);
97 spawn_local(async move {
98 if let Ok(found) = recommend_for_path(path).await {
99 if generation.try_get_value() == Some(current) {
100 let _ = result.try_set(Some(found));
101 }
102 }
103 });
104 });
105
106 move || {
107 let found = result
108 .try_get()
109 .flatten()
110 .filter(|found| !found.posts.is_empty())?;
111 let words = found.words.join("」「");
112 Some(view! {
113 <section class="not-found-maybe" aria-labelledby="not-found-maybe-title">
114 <h2 id="not-found-maybe-title" class="section-label">
115 "也许你在找"
116 </h2>
117 <ul>
118 {found
119 .posts
120 .into_iter()
121 .map(|post| {
122 let href = crate::routes::Route::PostDetail {
123 slug: post.slug.clone(),
124 }
125 .to_string();
126 let date = post
127 .published_at
128 .map(|at| {
129 view! {
130 <time class="tabular-nums">
131 {crate::utils::time::ymd(&at, '.')}
132 </time>
133 }
134 });
135 view! {
136 <li>
137 <a class="not-found-row" href=href>
138 <span>{post.title}</span>
139 {date}
140 </a>
141 </li>
142 }
143 })
144 .collect_view()}
145 </ul>
146 <p class="not-found-note">{format!("按地址里的词「{words}」匹配到的文章。")}</p>
147 </section>
148 })
149 }
150}