feat(frontend): add clean-code toast notifications
This commit is contained in:
@@ -1,35 +1,89 @@
|
||||
use leptos::*;
|
||||
use shared::NotificationLevel;
|
||||
|
||||
// ============================================================================
|
||||
// Toast Icons (Clean Code: Separation of Concerns)
|
||||
// ============================================================================
|
||||
|
||||
/// Returns the appropriate SVG icon for the notification level
|
||||
fn get_toast_icon(level: &NotificationLevel) -> impl IntoView {
|
||||
match level {
|
||||
NotificationLevel::Info => view! {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
}.into_view(),
|
||||
NotificationLevel::Success => view! {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
}.into_view(),
|
||||
NotificationLevel::Warning => view! {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
}.into_view(),
|
||||
NotificationLevel::Error => view! {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
}.into_view(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the DaisyUI alert class for the notification level
|
||||
fn get_alert_class(level: &NotificationLevel) -> &'static str {
|
||||
match level {
|
||||
NotificationLevel::Info => "alert-info",
|
||||
NotificationLevel::Success => "alert-success",
|
||||
NotificationLevel::Warning => "alert-warning",
|
||||
NotificationLevel::Error => "alert-error",
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Toast Components (Clean Code: Single Responsibility)
|
||||
// ============================================================================
|
||||
|
||||
/// Individual toast item component
|
||||
#[component]
|
||||
pub fn ToastContainer() -> impl IntoView {
|
||||
let store = use_context::<crate::store::TorrentStore>().expect("store not provided");
|
||||
let notifications = store.notifications;
|
||||
|
||||
fn ToastItem(
|
||||
level: NotificationLevel,
|
||||
message: String,
|
||||
) -> impl IntoView {
|
||||
let alert_class = get_alert_class(&level);
|
||||
|
||||
view! {
|
||||
<div class="toast toast-end toast-bottom z-[9999]">
|
||||
{move || notifications.get().into_iter().map(|item| {
|
||||
let alert_class = match item.notification.level {
|
||||
NotificationLevel::Info => "alert-info",
|
||||
NotificationLevel::Success => "alert-success",
|
||||
NotificationLevel::Warning => "alert-warning",
|
||||
NotificationLevel::Error => "alert-error",
|
||||
};
|
||||
|
||||
let icon = match item.notification.level {
|
||||
NotificationLevel::Info => view! { <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> },
|
||||
NotificationLevel::Success => view! { <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> },
|
||||
NotificationLevel::Warning => view! { <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg> },
|
||||
NotificationLevel::Error => view! { <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> },
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class={format!("alert {} shadow-lg transition-all duration-300 animate-in slide-in-from-bottom-5 fade-in", alert_class)}>
|
||||
{icon}
|
||||
<span>{item.notification.message}</span>
|
||||
</div>
|
||||
}
|
||||
}).collect::<Vec<_>>()}
|
||||
<div class={format!(
|
||||
"alert {} shadow-lg min-w-[280px] max-w-[400px] transition-all duration-300 animate-in slide-in-from-right fade-in",
|
||||
alert_class
|
||||
)}>
|
||||
{get_toast_icon(&level)}
|
||||
<span class="text-sm font-medium truncate">{message}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
/// Main toast container - renders all active notifications
|
||||
#[component]
|
||||
pub fn ToastContainer() -> impl IntoView {
|
||||
let store = use_context::<crate::store::TorrentStore>().expect("TorrentStore not provided");
|
||||
let notifications = store.notifications;
|
||||
|
||||
view! {
|
||||
<div class="toast toast-end toast-bottom z-[9999] gap-2">
|
||||
<For
|
||||
each=move || notifications.get()
|
||||
key=|item| item.id
|
||||
children=move |item| {
|
||||
view! {
|
||||
<ToastItem
|
||||
level=item.notification.level
|
||||
message=item.notification.message
|
||||
/>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use leptos::*;
|
||||
use leptos::html::Dialog;
|
||||
use crate::store::{toast_success, toast_error, toast_warning};
|
||||
|
||||
|
||||
#[component]
|
||||
@@ -22,6 +23,7 @@ pub fn AddTorrentModal(
|
||||
let handle_submit = move |_| {
|
||||
let uri_val = uri.get();
|
||||
if uri_val.is_empty() {
|
||||
toast_warning("Lütfen bir Magnet URI veya URL girin");
|
||||
set_error_msg.set(Some("Please enter a Magnet URI or URL".to_string()));
|
||||
return;
|
||||
}
|
||||
@@ -42,6 +44,7 @@ pub fn AddTorrentModal(
|
||||
Ok(resp) => {
|
||||
if resp.ok() {
|
||||
logging::log!("Torrent added successfully");
|
||||
toast_success("Torrent eklendi");
|
||||
set_loading.set(false);
|
||||
if let Some(dialog) = dialog_ref.get() {
|
||||
dialog.close();
|
||||
@@ -51,12 +54,14 @@ pub fn AddTorrentModal(
|
||||
let status = resp.status();
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
logging::error!("Failed to add torrent: {} - {}", status, text);
|
||||
toast_error("Torrent eklenemedi");
|
||||
set_error_msg.set(Some(format!("Error {}: {}", status, text)));
|
||||
set_loading.set(false);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
logging::error!("Network error: {}", e);
|
||||
toast_error("Bağlantı hatası");
|
||||
set_error_msg.set(Some(format!("Network Error: {}", e)));
|
||||
set_loading.set(false);
|
||||
}
|
||||
@@ -64,6 +69,7 @@ pub fn AddTorrentModal(
|
||||
}
|
||||
Err(e) => {
|
||||
logging::error!("Serialization error: {}", e);
|
||||
toast_error("İstek hatası");
|
||||
set_error_msg.set(Some(format!("Request Error: {}", e)));
|
||||
set_loading.set(false);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use leptos::*;
|
||||
use wasm_bindgen::closure::Closure;
|
||||
use wasm_bindgen::JsCast;
|
||||
use crate::store::{get_action_messages, toast_success, toast_error};
|
||||
|
||||
fn format_bytes(bytes: i64) -> String {
|
||||
const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||
@@ -181,6 +182,11 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
logging::log!("TorrentTable Action: {} on {}", action, hash);
|
||||
set_menu_visible.set(false); // Close menu immediately
|
||||
|
||||
// Get action messages for toast (Clean Code: DRY)
|
||||
let (success_msg, error_msg) = get_action_messages(&action);
|
||||
let success_msg = success_msg.to_string();
|
||||
let error_msg = error_msg.to_string();
|
||||
|
||||
spawn_local(async move {
|
||||
let action_req = if action == "delete_with_data" {
|
||||
"delete_with_data"
|
||||
@@ -204,13 +210,21 @@ pub fn TorrentTable() -> impl IntoView {
|
||||
resp.status(),
|
||||
resp.status_text()
|
||||
);
|
||||
toast_error(error_msg);
|
||||
} else {
|
||||
logging::log!("Action {} executed successfully", action);
|
||||
toast_success(success_msg);
|
||||
}
|
||||
}
|
||||
Err(e) => logging::error!("Network error executing action: {}", e),
|
||||
Err(e) => {
|
||||
logging::error!("Network error executing action: {}", e);
|
||||
toast_error(format!("{}: Bağlantı hatası", error_msg));
|
||||
}
|
||||
},
|
||||
Err(e) => logging::error!("Failed to serialize request: {}", e),
|
||||
Err(e) => {
|
||||
logging::error!("Failed to serialize request: {}", e);
|
||||
toast_error(error_msg);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user