From 6c7379483e784bea1716cd50257c08aebac20314 Mon Sep 17 00:00:00 2001 From: spinline Date: Thu, 5 Feb 2026 20:40:11 +0300 Subject: [PATCH] feat(frontend): add clean-code toast notifications --- frontend/src/components/toast.rs | 108 +++++++++++++----- .../src/components/torrent/add_torrent.rs | 6 + frontend/src/components/torrent/table.rs | 18 ++- frontend/src/store.rs | 68 ++++++++++- 4 files changed, 170 insertions(+), 30 deletions(-) diff --git a/frontend/src/components/toast.rs b/frontend/src/components/toast.rs index ee8e906..8a845a6 100644 --- a/frontend/src/components/toast.rs +++ b/frontend/src/components/toast.rs @@ -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! { + + + + }.into_view(), + NotificationLevel::Success => view! { + + + + }.into_view(), + NotificationLevel::Warning => view! { + + + + }.into_view(), + NotificationLevel::Error => view! { + + + + }.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::().expect("store not provided"); - let notifications = store.notifications; - +fn ToastItem( + level: NotificationLevel, + message: String, +) -> impl IntoView { + let alert_class = get_alert_class(&level); + view! { -
- {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! { }, - NotificationLevel::Success => view! { }, - NotificationLevel::Warning => view! { }, - NotificationLevel::Error => view! { }, - }; - - view! { -
- {icon} - {item.notification.message} -
- } - }).collect::>()} +
+ {get_toast_icon(&level)} + {message} +
+ } +} + +/// Main toast container - renders all active notifications +#[component] +pub fn ToastContainer() -> impl IntoView { + let store = use_context::().expect("TorrentStore not provided"); + let notifications = store.notifications; + + view! { +
+ + } + } + />
} } diff --git a/frontend/src/components/torrent/add_torrent.rs b/frontend/src/components/torrent/add_torrent.rs index 2f51cca..54bb278 100644 --- a/frontend/src/components/torrent/add_torrent.rs +++ b/frontend/src/components/torrent/add_torrent.rs @@ -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); } diff --git a/frontend/src/components/torrent/table.rs b/frontend/src/components/torrent/table.rs index 6e50987..4424f43 100644 --- a/frontend/src/components/torrent/table.rs +++ b/frontend/src/components/torrent/table.rs @@ -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); + } } }); }; diff --git a/frontend/src/store.rs b/frontend/src/store.rs index a51abcf..3045f08 100644 --- a/frontend/src/store.rs +++ b/frontend/src/store.rs @@ -1,7 +1,7 @@ use futures::StreamExt; use gloo_net::eventsource::futures::EventSource; use leptos::*; -use shared::{AppEvent, GlobalStats, SystemNotification, Torrent}; +use shared::{AppEvent, GlobalStats, NotificationLevel, SystemNotification, Torrent}; #[derive(Clone, Debug, PartialEq)] pub struct NotificationItem { @@ -9,6 +9,72 @@ pub struct NotificationItem { pub notification: SystemNotification, } +// ============================================================================ +// Toast Helper Functions (Clean Code: Single Responsibility) +// ============================================================================ + +/// Shows a toast notification with the given level and message. +/// Auto-removes after 5 seconds. +pub fn show_toast(level: NotificationLevel, message: impl Into) { + if let Some(store) = use_context::() { + let id = js_sys::Date::now() as u64; + let notification = SystemNotification { + level, + message: message.into(), + }; + let item = NotificationItem { id, notification }; + + store.notifications.update(|list| list.push(item)); + + // Auto-remove after 5 seconds + let notifications = store.notifications; + let _ = set_timeout( + move || { + notifications.update(|list| list.retain(|i| i.id != id)); + }, + std::time::Duration::from_secs(5), + ); + } +} + +/// Convenience function for success toasts +pub fn toast_success(message: impl Into) { + show_toast(NotificationLevel::Success, message); +} + +/// Convenience function for error toasts +pub fn toast_error(message: impl Into) { + show_toast(NotificationLevel::Error, message); +} + +/// Convenience function for info toasts +pub fn toast_info(message: impl Into) { + show_toast(NotificationLevel::Info, message); +} + +/// Convenience function for warning toasts +pub fn toast_warning(message: impl Into) { + show_toast(NotificationLevel::Warning, message); +} + +// ============================================================================ +// Action Message Mapping (Clean Code: DRY Principle) +// ============================================================================ + +/// Maps torrent action strings to user-friendly Turkish messages. +/// Returns (success_message, error_message) +pub fn get_action_messages(action: &str) -> (&'static str, &'static str) { + match action { + "start" => ("Torrent başlatıldı", "Torrent başlatılamadı"), + "stop" => ("Torrent durduruldu", "Torrent durdurulamadı"), + "pause" => ("Torrent duraklatıldı", "Torrent duraklatılamadı"), + "delete" => ("Torrent silindi", "Torrent silinemedi"), + "delete_with_data" => ("Torrent ve verileri silindi", "Torrent silinemedi"), + "recheck" => ("Torrent kontrol ediliyor", "Kontrol başlatılamadı"), + _ => ("İşlem tamamlandı", "İşlem başarısız"), + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FilterStatus { All,