htmx_components/server/
notification.rs1use super::transition::Transition;
2use super::yc_control::YcControlJsApi;
3use rscx::{component, html, props, CollectFragmentAsync};
4
5#[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 </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#[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", };
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), Template,
145 TemplateSelector(String),
146}
147
148#[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#[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}