dioxus_docs_kit/components/blog/
search_modal.rs1use dioxus::prelude::*;
2use dioxus_free_icons::Icon;
3use dioxus_free_icons::icons::ld_icons::{LdSearch, LdX};
4
5use crate::BlogContext;
6use crate::blog::registry::BlogRegistry;
7
8#[component]
10pub fn BlogSearchModal() -> Element {
11 let mut search_open = use_context::<Signal<bool>>();
12 let mut query = use_signal(String::new);
13 let ctx = use_context::<BlogContext>();
14 let registry = use_context::<&'static BlogRegistry>();
15
16 let results = use_memo(move || registry.search_posts(&query()));
17
18 let on_keydown = move |e: KeyboardEvent| {
19 if e.key() == Key::Enter {
20 let results = results.read();
21 if let Some(entry) = results.first() {
22 (ctx.navigate)(entry.slug.clone());
23 search_open.set(false);
24 query.set(String::new());
25 }
26 } else if e.key() == Key::Escape {
27 search_open.set(false);
28 query.set(String::new());
29 }
30 };
31
32 if !search_open() {
33 return rsx! {};
34 }
35
36 rsx! {
37 div {
38 class: "fixed inset-0 z-[100] bg-black/50 flex items-start justify-center pt-[15vh]",
39 onclick: move |_| {
40 search_open.set(false);
41 query.set(String::new());
42 },
43
44 div {
45 class: "bg-base-200 rounded-xl w-full max-w-lg mx-4 border border-base-300 shadow-2xl overflow-hidden",
46 onclick: move |e| e.stop_propagation(),
47
48 div { class: "flex items-center gap-3 px-4 py-3 border-b border-base-300",
49 Icon { class: "size-5 text-base-content/50 shrink-0", icon: LdSearch }
50 input {
51 class: "flex-1 bg-transparent outline-none text-base placeholder:text-base-content/40",
52 placeholder: "Search posts...",
53 autofocus: true,
54 value: "{query}",
55 oninput: move |e| query.set(e.value()),
56 onkeydown: on_keydown,
57 }
58 button {
59 class: "btn btn-ghost btn-xs btn-square",
60 onclick: move |_| {
61 search_open.set(false);
62 query.set(String::new());
63 },
64 Icon { class: "size-4", icon: LdX }
65 }
66 }
67
68 div { class: "max-h-80 overflow-y-auto",
69 if query().trim().is_empty() {
70 div { class: "px-4 py-8 text-center text-base-content/50 text-sm",
71 "Type to search..."
72 }
73 } else if results.read().is_empty() {
74 div { class: "px-4 py-8 text-center text-base-content/50 text-sm",
75 "No results for \"{query}\""
76 }
77 } else {
78 for entry in results.read().iter() {
79 {
80 let slug = entry.slug.clone();
81 let title = entry.title.clone();
82 let date = entry.date.clone();
83 let tags = entry.tags.clone();
84 rsx! {
85 BlogSearchResultItem {
86 slug,
87 title,
88 date,
89 tags,
90 search_open,
91 query,
92 }
93 }
94 }
95 }
96 }
97 }
98
99 div { class: "px-4 py-2 border-t border-base-300 text-xs text-base-content/40 flex justify-between",
100 span { "Esc to close" }
101 span { "Enter to navigate" }
102 }
103 }
104 }
105 }
106}
107
108#[component]
109fn BlogSearchResultItem(
110 slug: String,
111 title: String,
112 date: String,
113 tags: Vec<String>,
114 mut search_open: Signal<bool>,
115 mut query: Signal<String>,
116) -> Element {
117 let ctx = use_context::<BlogContext>();
118 let slug_for_click = slug.clone();
119
120 rsx! {
121 button {
122 class: "w-full text-left px-4 py-3 hover:bg-base-300/50 transition-colors flex items-center gap-3 border-b border-base-300/50 last:border-b-0",
123 onclick: move |_| {
124 (ctx.navigate)(slug_for_click.clone());
125 search_open.set(false);
126 query.set(String::new());
127 },
128 div { class: "flex-1 min-w-0",
129 div { class: "flex items-center gap-2",
130 span { class: "font-medium text-sm truncate", "{title}" }
131 }
132 div { class: "flex items-center gap-2 mt-0.5",
133 span { class: "text-xs text-base-content/50", "{date}" }
134 for tag in tags.iter() {
135 span { class: "badge badge-xs badge-outline badge-primary", "{tag}" }
136 }
137 }
138 }
139 }
140 }
141}