use leptos::prelude::*;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ToastType {
Info,
Success,
Warning,
Error,
}
impl ToastType {
fn icon(&self) -> &'static str {
match self {
ToastType::Info => "ℹ️",
ToastType::Success => "✅",
ToastType::Warning => "⚠️",
ToastType::Error => "❌",
}
}
fn class(&self) -> &'static str {
match self {
ToastType::Info => "toast-info",
ToastType::Success => "toast-success",
ToastType::Warning => "toast-warning",
ToastType::Error => "toast-error",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Toast {
pub id: usize,
pub message: String,
pub toast_type: ToastType,
pub auto_dismiss_ms: Option<u64>,
}
impl Toast {
pub fn new(id: usize, message: String, toast_type: ToastType) -> Self {
Self {
id,
message,
toast_type,
auto_dismiss_ms: Some(3000), }
}
pub fn info(id: usize, message: String) -> Self {
Self::new(id, message, ToastType::Info)
}
pub fn success(id: usize, message: String) -> Self {
Self::new(id, message, ToastType::Success)
}
pub fn warning(id: usize, message: String) -> Self {
Self::new(id, message, ToastType::Warning)
}
pub fn error(id: usize, message: String) -> Self {
let mut toast = Self::new(id, message, ToastType::Error);
toast.auto_dismiss_ms = Some(5000); toast
}
}
#[derive(Clone, Copy)]
pub struct ToastContext {
toasts: RwSignal<Vec<Toast>>,
next_id: RwSignal<usize>,
}
impl ToastContext {
pub fn new() -> Self {
Self {
toasts: RwSignal::new(Vec::new()),
next_id: RwSignal::new(0),
}
}
pub fn show(&self, message: String, toast_type: ToastType) {
let id = self.next_id.get();
self.next_id.update(|n| *n += 1);
let toast = Toast::new(id, message, toast_type);
let auto_dismiss_ms = toast.auto_dismiss_ms;
self.toasts.update(|toasts| {
toasts.push(toast.clone());
});
if let Some(ms) = auto_dismiss_ms {
let toasts = self.toasts;
set_timeout(
move || {
toasts.update(|toasts| {
toasts.retain(|t| t.id != id);
});
},
Duration::from_millis(ms),
);
}
}
pub fn info(&self, message: String) {
self.show(message, ToastType::Info);
}
pub fn success(&self, message: String) {
self.show(message, ToastType::Success);
}
pub fn warning(&self, message: String) {
self.show(message, ToastType::Warning);
}
pub fn error(&self, message: String) {
self.show(message, ToastType::Error);
}
pub fn dismiss(&self, id: usize) {
self.toasts.update(|toasts| {
toasts.retain(|t| t.id != id);
});
}
pub fn toasts(&self) -> Vec<Toast> {
self.toasts.get()
}
}
impl Default for ToastContext {
fn default() -> Self {
Self::new()
}
}
#[component]
pub fn ToastProvider(children: Children) -> impl IntoView {
let toast_context = ToastContext::new();
provide_context(toast_context);
view! {
{children()}
<ToastContainer />
}
}
#[component]
pub fn ToastContainer() -> impl IntoView {
let toast_context = expect_context::<ToastContext>();
view! {
<div class="toast-container">
<For
each=move || toast_context.toasts()
key=|toast| toast.id
children=move |toast| {
view! { <ToastItem toast=toast /> }
}
/>
</div>
}
}
#[component]
fn ToastItem(toast: Toast) -> impl IntoView {
let toast_context = expect_context::<ToastContext>();
let id = toast.id;
let message = toast.message.clone();
let icon = toast.toast_type.icon();
let class = toast.toast_type.class();
view! {
<div class=format!("toast {}", class)>
<div class="toast-content">
<span class="toast-icon">{icon}</span>
<span class="toast-message">{message}</span>
</div>
<button
class="toast-close"
on:click=move |_| toast_context.dismiss(id)
aria-label="Dismiss"
>
"×"
</button>
</div>
}
}
pub fn use_toast() -> ToastContext {
expect_context::<ToastContext>()
}