1use std::collections::HashMap;
2
3use chrono::Duration;
4use leptos::*;
5
6pub type MessageKey = usize;
7
8#[derive(Debug, Clone, PartialEq)]
9struct Notification {
10 kind: NotificationKind,
11 do_fade: bool,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15enum NotificationKind {
16 Message(bool, View),
17 Error(bool, View),
18 Success(bool, View),
19}
20
21impl NotificationKind {
22 fn get_view(&self) -> Option<View> {
23 match self {
24 NotificationKind::Message(_, msg) => Some(msg.clone()),
25 NotificationKind::Error(_, msg) => Some(msg.clone()),
26 NotificationKind::Success(_, msg) => Some(msg.clone()),
27 }
28 }
29}
30
31pub trait Handle: Clone + Copy + 'static {}
32#[derive(Debug, Clone, Copy)]
33pub struct WithHandle;
34impl Handle for WithHandle {}
35#[derive(Debug, Clone, Copy)]
36pub struct NoHandle;
37impl Handle for NoHandle {}
38
39#[derive(Debug, Clone, Copy)]
40pub struct MessageJar<T: Handle> {
41 messages: RwSignal<HashMap<MessageKey, Notification>>,
42 reset_time: Option<Duration>,
43 next_key: RwSignal<MessageKey>,
44 as_modal: bool,
45 phantomdata: std::marker::PhantomData<T>,
46}
47
48#[allow(dead_code)]
49impl<T: Handle + 'static> MessageJar<T> {
50 pub fn new(reset_time: Duration) -> Self {
51 Self {
52 messages: HashMap::new().into(),
53 reset_time: Some(reset_time),
54 as_modal: false,
55 next_key: 0.into(),
56 phantomdata: std::marker::PhantomData {},
57 }
58 }
59
60 fn get_ordered(&self) -> Signal<Vec<(MessageKey, Notification)>> {
61 create_read_slice(self.messages, |msgs| {
62 let mut entries = msgs
63 .iter()
64 .map(|(key, value)| (*key, value.clone()))
65 .collect::<Vec<_>>();
66 entries.sort_by(|a, b| a.0.cmp(&b.0));
67 entries
68 })
69 }
70
71 pub fn is_emtpy(&self) -> bool {
72 self.messages.get().is_empty()
73 }
74
75 pub fn clear(&self) {
76 self.messages.update(|list| list.clear())
77 }
78
79 pub fn without_timeout(self) -> Self {
80 Self {
81 reset_time: None,
82 ..self
83 }
84 }
85
86 pub fn with_timeout(self, reset_time: Duration) -> Self {
87 Self {
88 reset_time: Some(reset_time),
89 ..self
90 }
91 }
92
93 pub fn as_modal(self) -> Self {
94 Self {
95 as_modal: true,
96 ..self
97 }
98 }
99
100 fn add_msg(&self, msg: NotificationKind) -> MessageKey {
101 self.next_key.update(|k| *k += 1);
102 let key = self.next_key.get_untracked();
103 self.messages.update(|m| {
104 m.insert(
105 key,
106 Notification {
107 kind: msg,
108 do_fade: false,
109 },
110 );
111 });
112 key
113 }
114
115 fn msg_timeout_effect(self, key: MessageKey) {
116 if let Some(timeout) = self.reset_time {
117 set_timeout(move || self.fade_out(key), timeout.to_std().unwrap())
118 }
119 }
120
121 pub fn fade_out(self, key: MessageKey) {
122 self.messages.update(|m| {
123 if let Some(v) = m.get_mut(&key) {
124 v.do_fade = true
125 }
126 })
127 }
128
129 pub fn get_last_key(self) -> Signal<MessageKey> {
130 Signal::derive(self.next_key)
131 }
132}
133
134impl MessageJar<NoHandle> {
135 pub fn with_handle(self) -> MessageJar<WithHandle> {
136 unsafe { std::mem::transmute(self) }
137 }
138
139 pub fn set_msg(self, msg: impl ToString) {
140 let msg = msg.to_string();
141 let msg_lines = msg.lines();
142 let key = self.add_msg(NotificationKind::Message(
143 self.as_modal,
144 msg_lines
145 .map(|l| view! { <b>{l.to_string()}</b> })
146 .collect_view(),
147 ));
148 self.msg_timeout_effect(key);
149 }
150
151 pub fn set_msg_view(self, msg: impl IntoView + 'static) {
152 let msg = msg.into_view();
153 let key = self.add_msg(NotificationKind::Message(self.as_modal, msg.clone()));
154 self.msg_timeout_effect(key);
155 }
156
157 pub fn set_success(&self, msg: &str) {
158 let msg_lines = msg.lines();
159 let key = self.add_msg(NotificationKind::Success(
160 self.as_modal,
161 msg_lines
162 .map(|l| view! { <b>{l.to_string()}</b> })
163 .collect_view(),
164 ));
165
166 self.msg_timeout_effect(key)
167 }
168
169 pub fn set_success_view(&self, msg: impl IntoView) {
170 let key = self.add_msg(NotificationKind::Success(self.as_modal, msg.into_view()));
171 self.msg_timeout_effect(key);
172 }
173
174 pub fn set_err(self, err: impl ToString) {
175 let err = err.to_string();
176 let msg_lines = err.lines();
177 let key = self.add_msg(NotificationKind::Error(
178 self.as_modal,
179 msg_lines
180 .map(|l| view! { <b>{l.to_string()}</b> })
181 .collect_view(),
182 ));
183 self.msg_timeout_effect(key);
184 }
185
186 pub fn set_err_view(&self, err: impl IntoView) {
187 let key = self.add_msg(NotificationKind::Error(self.as_modal, err.into_view()));
188 self.msg_timeout_effect(key)
189 }
190
191 pub fn set_server_err(&self, err: &leptos::ServerFnError) {
192 match err {
193 ServerFnError::WrappedServerError(e) => self.set_err(e),
194 ServerFnError::Registration(e) => self.set_err(e),
195 ServerFnError::Request(e) => self.set_err(e),
196 ServerFnError::Response(e) => self.set_err(e),
197 ServerFnError::ServerError(e) => self.set_err(e),
198 ServerFnError::Deserialization(e) => self.set_err(e),
199 ServerFnError::Serialization(e) => self.set_err(e),
200 ServerFnError::Args(e) => self.set_err(e),
201 ServerFnError::MissingArg(e) => self.set_err(e),
202 }
203 }
204}
205
206impl MessageJar<WithHandle> {
207 pub fn set_msg(self, msg: impl ToString) -> MessageKey {
208 let msg = msg.to_string();
209 let msg_lines = msg.lines();
210 let key = self.add_msg(NotificationKind::Message(
211 self.as_modal,
212 msg_lines
213 .map(|l| view! { <b>{l.to_string()}</b> })
214 .collect_view(),
215 ));
216 self.msg_timeout_effect(key);
217 key
218 }
219
220 pub fn set_msg_view(self, msg: impl IntoView + 'static) -> MessageKey {
221 let msg = msg.into_view();
222 let key = self.add_msg(NotificationKind::Message(self.as_modal, msg.clone()));
223 self.msg_timeout_effect(key);
224 key
225 }
226
227 pub fn set_success(&self, msg: &str) -> MessageKey {
228 let msg_lines = msg.lines();
229 let key = self.add_msg(NotificationKind::Success(
230 self.as_modal,
231 msg_lines
232 .map(|l| view! { <b>{l.to_string()}</b> })
233 .collect_view(),
234 ));
235
236 self.msg_timeout_effect(key);
237 key
238 }
239
240 pub fn set_success_view(&self, msg: impl IntoView) -> MessageKey {
241 let key = self.add_msg(NotificationKind::Success(self.as_modal, msg.into_view()));
242 self.msg_timeout_effect(key);
243 key
244 }
245
246 pub fn set_err(self, err: impl ToString) -> MessageKey {
247 let err = err.to_string();
248 let msg_lines = err.lines();
249 let key = self.add_msg(NotificationKind::Error(
250 self.as_modal,
251 msg_lines
252 .map(|l| view! { <b>{l.to_string()}</b> })
253 .collect_view(),
254 ));
255 self.msg_timeout_effect(key);
256 key
257 }
258
259 pub fn set_err_view(&self, err: impl IntoView) -> MessageKey {
260 let key = self.add_msg(NotificationKind::Error(self.as_modal, err.into_view()));
261 self.msg_timeout_effect(key);
262 key
263 }
264
265 pub fn set_server_err(&self, err: &leptos::ServerFnError) -> MessageKey {
266 match err {
267 ServerFnError::WrappedServerError(e) => self.set_err(e),
268 ServerFnError::Registration(e) => self.set_err(e),
269 ServerFnError::Request(e) => self.set_err(e),
270 ServerFnError::Response(e) => self.set_err(e),
271 ServerFnError::ServerError(e) => self.set_err(e),
272 ServerFnError::Deserialization(e) => self.set_err(e),
273 ServerFnError::Serialization(e) => self.set_err(e),
274 ServerFnError::Args(e) => self.set_err(e),
275 ServerFnError::MissingArg(e) => self.set_err(e),
276 }
277 }
278}
279
280#[component]
281fn Message(key: MessageKey, jar: MessageJar<NoHandle>) -> impl IntoView {
282 if !jar.messages.get_untracked().contains_key(&key) {
283 return view! {}.into_view();
284 }
285
286 let kind = create_read_slice(jar.messages, move |map| map.get(&key).unwrap().kind.clone());
287
288 let border_style = move || match kind() {
289 NotificationKind::Message(_, _) => "border: 2px solid #ffe135",
290 NotificationKind::Error(_, _) => "color: tomato; border: 2px solid tomato;",
291 NotificationKind::Success(_, _) => "color: #28a745; border: 2px solid #28a745;",
292 };
293
294 let is_modal = move || match kind() {
295 NotificationKind::Message(is_modal, _) => is_modal,
296 NotificationKind::Error(is_modal, _) => is_modal,
297 NotificationKind::Success(is_modal, _) => is_modal,
298 };
299
300 let dialog_ref = create_node_ref::<html::Dialog>();
301 create_effect(move |_| {
302 if let Some(d) = dialog_ref() {
303 d.close();
304 if is_modal() {
305 let _ = d.show_modal();
306 } else {
307 d.show();
308 }
309 }
310 });
311
312 let dialog_class = create_read_slice(jar.messages, move |map| {
313 if map.get(&key).unwrap().do_fade {
314 String::from("fade-out")
315 } else {
316 String::from("")
317 }
318 });
319
320 let on_close_click = move |ev: ev::MouseEvent| {
321 ev.stop_propagation();
322 jar.fade_out(key)
323 };
324
325 let on_animend = move |_| {
326 jar.messages.update(|m| {
327 m.remove(&key);
328 })
329 };
330
331 view! {
332 <dialog
333 on:click=|ev| ev.stop_propagation()
334 node_ref=dialog_ref
335 class=dialog_class
336 style=border_style
337 on:animationend=on_animend
338 >
339 <div class="content">
340 <button class="close" on:click=on_close_click>
341 <i class="fa-solid fa-xmark"></i>
342 </button>
343 {move || kind.get().get_view().unwrap_or(view! {}.into_view())}
344 </div>
345 </dialog>
346 }
347 .into_view()
348}
349
350#[component]
351pub fn ProvideMessageSystem() -> impl IntoView {
352 let msg_jar = MessageJar::new(Duration::seconds(5));
353 provide_context(msg_jar);
354
355 view! {
363 <Show when=move || !msg_jar.is_emtpy()>
364 <notification-box>
365 <For
366 each=move || msg_jar.get_ordered().get().into_iter().rev()
367 key=|(key, _)| *key
368 children=move |(key, _)| {
369 view! { <Message key jar=msg_jar /> }
370 }
371 />
372
373 </notification-box>
374 </Show>
375 }
376}