htmx_components/server/
notification.rs

1use super::transition::Transition;
2use super::yc_control::YcControlJsApi;
3use rscx::{component, html, props, CollectFragmentAsync};
4
5/**
6 * NotificationLiveRegion
7 *
8 * Holds all attached notifications to show
9 * Also contains all standard templates.
10 */
11
12#[component]
13pub fn NotificationLiveRegion() -> String {
14    html! {
15        <div id="notification-live-region" aria-live="assertive" class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6">
16            <section class="flex w-full flex-col items-center space-y-4 sm:items-end" data-notification-content>
17            </section>
18            <template id="tpl-notification">
19                <SimpleNotification icon_svg=IconSvg::Info />
20            </template>
21            <template id="tpl-notification-icons">
22                <NotificationIcon svg=IconSvg::Success/>
23                <NotificationIcon svg=IconSvg::Error/>
24                <NotificationIcon svg=IconSvg::Info/>
25                // Add any additional prerendered icons here.
26            </template>
27        </div>
28    }
29}
30
31#[props]
32pub struct SimpleNotificationProps {
33    #[builder(setter(into), default="Notification".to_string())]
34    title: String,
35
36    #[builder(setter(into), default)]
37    message: String,
38
39    #[builder(setter(into))]
40    icon_svg: IconSvg,
41}
42
43#[component]
44pub fn SimpleNotification(props: SimpleNotificationProps) -> String {
45    html! {
46        <NotificationTransition
47            class="w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5"
48        >
49            <div class="p-4">
50                <div class="flex items-start">
51                    <div class="flex-shrink-0">
52                        <NotificationIcon svg=props.icon_svg />
53                    </div>
54                    <div class="ml-3 w-0 flex-1 pt-0.5">
55                        <p class="text-sm font-medium text-gray-900" data-notification-title>{props.title}</p>
56                        <p class="mt-1 text-sm text-gray-500" data-notification-message>{props.message}</p>
57                    </div>
58                    <NoticationCloseButton />
59                </div>
60            </div>
61        </NotificationTransition>
62    }
63}
64
65pub enum IconSvg {
66    Success,
67    Error,
68    Info,
69    Custom(String),
70}
71
72impl From<String> for IconSvg {
73    fn from(s: String) -> Self {
74        IconSvg::Custom(s)
75    }
76}
77
78#[props]
79struct NotificationIconProps {
80    svg: IconSvg,
81}
82
83#[component]
84fn NotificationIcon(props: NotificationIconProps) -> String {
85    match props.svg {
86        IconSvg::Success => html! {
87            <svg class="h-6 w-6 text-green-400" data-notification-icon="success" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
88                <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
89            </svg>
90        },
91        IconSvg::Error => html! {
92            <svg class="h-6 w-6 text-red-400" data-notification-icon="error" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
93                <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
94            </svg>
95        },
96        IconSvg::Info => html! {
97            <svg class="h-6 w-6 text-blue-400" data-notification-icon="info" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
98                <path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
99            </svg>
100        },
101        IconSvg::Custom(svg) => svg,
102    }
103}
104
105// #### Notification components for use with axum resources. ###############
106
107/**
108 * NotificationFlashes
109 *
110 * Use this to present a axum_flash message with a notification
111 */
112
113#[props]
114pub struct NotificationFlashesProps {
115    flashes: axum_flash::IncomingFlashes,
116}
117
118#[component]
119pub fn NotificationFlashes(props: NotificationFlashesProps) -> String {
120    props
121        .flashes
122        .into_iter()
123        .map(|(level, message)| async move {
124            let js_notification_fn = match level {
125                axum_flash::Level::Success => "showSuccessNotification",
126                axum_flash::Level::Error => "showErrorNotification",
127                _ => "showErrorNotification", // TODO! Replace with generic notification.
128            };
129
130            let message = serde_json::to_string(&message).unwrap();
131
132            html! {
133                <YcControlJsApi call=format!("{}({})", js_notification_fn, message) />
134            }
135        })
136        .collect_fragment_async()
137        .await
138}
139
140pub enum NotificationCall {
141    Success(String),
142    Error(String),
143    Info(String, String), // title, message
144    Template,
145    TemplateSelector(String),
146}
147
148/**
149 * NotificationPresenter
150 *
151 * Use this to present a notification from a server resource.
152 */
153
154#[props]
155pub struct NotificationPresenterProps {
156    call: NotificationCall,
157
158    #[builder(default)]
159    children: String,
160}
161
162fn js_enc<T>(data: &T) -> String
163where
164    T: ?Sized + serde::Serialize,
165{
166    serde_json::to_string::<T>(data).unwrap()
167}
168
169#[component]
170pub fn NotificationPresenter(props: NotificationPresenterProps) -> String {
171    let api_call = match props.call {
172        NotificationCall::Success(message) => {
173            format!("showSuccessNotification({})", js_enc(&message))
174        }
175        NotificationCall::Error(message) => {
176            format!("showErrorNotification({})", js_enc(&message))
177        }
178        NotificationCall::Info(title, message) => {
179            format!("showNotification({}, {})", js_enc(&title), js_enc(&message))
180        }
181        NotificationCall::TemplateSelector(templateSelector) => {
182            format!(
183                "showNotificationWithTemplate({})",
184                js_enc(&templateSelector),
185            )
186        }
187        NotificationCall::Template => {
188            if props.children.is_empty() {
189                panic!("NotificationPresenter: Template call requires children.")
190            }
191            "showNotificationWithTemplate(callerScript.nextElementSibling)".into()
192        }
193    };
194
195    html! {
196        <YcControlJsApi call=api_call />
197        {props.children}
198    }
199}
200
201// #### Notification components to help you build your own. ###############
202
203#[props]
204pub struct NotificationTransitionProps {
205    #[builder(setter(into), default="".to_string())]
206    class: String,
207
208    #[builder(default)]
209    children: String,
210}
211
212#[component]
213pub fn NotificationTransition(props: NotificationTransitionProps) -> String {
214    html! {
215        <Transition
216            class=format!("pointer-events-auto {}", props.class).trim()
217            enter="transform ease-out duration-300 transition"
218            enter_from="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
219            enter_to="translate-y-0 opacity-100 sm:translate-x-0"
220            leave="transition ease-in duration-300"
221            leave_from="opacity-100"
222            leave_to="opacity-0"
223        >
224            {props.children}
225        </Transition>
226    }
227}
228
229#[component]
230pub fn NoticationCloseButton() -> String {
231    html! {
232        <div class="ml-4 flex flex-shrink-0">
233            <button type="button" data-toggle-action="close" data-notification-close class="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
234                <span class="sr-only">Close</span>
235                <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" data-notification-close>
236                    <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
237                </svg>
238            </button>
239        </div>
240    }
241}