nightshade_api/web/base/
feedback.rs1use leptos::prelude::*;
2use wasm_bindgen::JsCast;
3use wasm_bindgen::prelude::*;
4
5#[component]
7pub fn Spinner() -> impl IntoView {
8 view! { <span class="nightshade-spinner"></span> }
9}
10
11#[derive(Clone)]
13pub struct Toast {
14 pub id: usize,
16 pub text: String,
18 pub kind: &'static str,
20 pub action: Option<(String, Callback<()>)>,
22}
23
24#[derive(Clone, Copy)]
27pub struct Toaster {
28 toasts: RwSignal<Vec<Toast>>,
29 next: RwSignal<usize>,
30}
31
32impl Toaster {
33 pub fn info(&self, text: impl Into<String>) {
35 self.push(text.into(), "info", 3200, None);
36 }
37
38 pub fn success(&self, text: impl Into<String>) {
40 self.push(text.into(), "success", 3200, None);
41 }
42
43 pub fn warning(&self, text: impl Into<String>) {
45 self.push(text.into(), "warn", 4200, None);
46 }
47
48 pub fn error(&self, text: impl Into<String>) {
50 self.push(text.into(), "error", 5000, None);
51 }
52
53 pub fn action(
56 &self,
57 text: impl Into<String>,
58 label: impl Into<String>,
59 on_action: Callback<()>,
60 ) {
61 self.push(text.into(), "info", 6000, Some((label.into(), on_action)));
62 }
63
64 fn push(
65 &self,
66 text: String,
67 kind: &'static str,
68 duration_ms: i32,
69 action: Option<(String, Callback<()>)>,
70 ) {
71 let id = self.next.get_untracked();
72 self.next.set(id + 1);
73 self.toasts.update(|list| {
74 list.push(Toast {
75 id,
76 text,
77 kind,
78 action,
79 })
80 });
81 let toasts = self.toasts;
82 after(duration_ms, move || {
83 toasts.update(|list| list.retain(|toast| toast.id != id));
84 });
85 }
86
87 fn dismiss(&self, id: usize) {
88 self.toasts
89 .update(|list| list.retain(|toast| toast.id != id));
90 }
91}
92
93pub fn use_toaster() -> Toaster {
96 use_context::<Toaster>().unwrap_or_else(|| Toaster {
97 toasts: RwSignal::new(Vec::new()),
98 next: RwSignal::new(0),
99 })
100}
101
102#[component]
106pub fn ToastHub(children: Children) -> impl IntoView {
107 let toaster = Toaster {
108 toasts: RwSignal::new(Vec::new()),
109 next: RwSignal::new(0),
110 };
111 provide_context(toaster);
112 view! {
113 {children()}
114 <div class="nightshade-toast-stack">
115 <For each=move || toaster.toasts.get() key=|toast| toast.id let:toast>
116 {
117 let id = toast.id;
118 let action = toast.action.clone();
119 view! {
120 <div class=format!("nightshade-toast {}", toast.kind)>
121 <span class="nightshade-toast-text">{toast.text.clone()}</span>
122 {action
123 .map(|(label, callback)| {
124 view! {
125 <button
126 class="nightshade-toast-action"
127 on:click=move |_| {
128 callback.run(());
129 toaster.dismiss(id);
130 }
131 >
132 {label}
133 </button>
134 }
135 })}
136 <button
137 class="nightshade-toast-close"
138 aria-label="Dismiss"
139 on:click=move |_| toaster.dismiss(id)
140 >
141 "\u{00d7}"
142 </button>
143 </div>
144 }
145 }
146 </For>
147 </div>
148 }
149}
150
151fn after(milliseconds: i32, callback: impl FnOnce() + 'static) {
152 let closure = Closure::once_into_js(callback);
153 if let Some(window) = web_sys::window() {
154 let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
155 closure.unchecked_ref(),
156 milliseconds,
157 );
158 }
159}