Compare commits
5 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
907ae66a7f | ||
|
|
f35b119c0d | ||
|
|
920704ee72 | ||
|
|
d8ad9e62d8 | ||
|
|
ea99ac62bc |
@@ -17,6 +17,7 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
gloo-net = "0.6"
|
gloo-net = "0.6"
|
||||||
gloo-timers = { version = "0.3", features = ["futures"] }
|
gloo-timers = { version = "0.3", features = ["futures"] }
|
||||||
|
gloo-console = "0.3"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
uuid = { version = "1", features = ["v4", "js"] }
|
uuid = { version = "1", features = ["v4", "js"] }
|
||||||
@@ -49,7 +50,7 @@ leptos-shadcn-scroll-area = "0.8"
|
|||||||
leptos-shadcn-dialog = "0.8"
|
leptos-shadcn-dialog = "0.8"
|
||||||
leptos-shadcn-label = "0.8"
|
leptos-shadcn-label = "0.8"
|
||||||
leptos-shadcn-alert = "0.8"
|
leptos-shadcn-alert = "0.8"
|
||||||
leptos-shadcn-toast = "0.8"
|
|
||||||
leptos-shadcn-dropdown-menu = "0.8"
|
leptos-shadcn-dropdown-menu = "0.8"
|
||||||
leptos-shadcn-tooltip = "0.8"
|
leptos-shadcn-tooltip = "0.8"
|
||||||
leptos-shadcn-skeleton = "0.8"
|
leptos-shadcn-skeleton = "0.8"
|
||||||
46
frontend/package-lock.json
generated
46
frontend/package-lock.json
generated
@@ -9,7 +9,11 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/cli": "^4.1.18"
|
"@tailwindcss/cli": "^4.1.18",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
@@ -1958,6 +1962,18 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/class-variance-authority": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://polar.sh/cva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cliui": {
|
"node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
@@ -1973,6 +1989,15 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -3637,12 +3662,31 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwind-merge": {
|
||||||
|
"version": "3.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
|
||||||
|
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/dcastil"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.18",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwindcss-animate": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": ">=3.0.0 || insiders"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
"tailwindcss": "^4.1.18"
|
"tailwindcss": "^4.1.18"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/cli": "^4.1.18"
|
"@tailwindcss/cli": "^4.1.18",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,20 @@ use leptos::task::spawn_local;
|
|||||||
use leptos_router::components::{Router, Routes, Route};
|
use leptos_router::components::{Router, Routes, Route};
|
||||||
use leptos_router::hooks::use_navigate;
|
use leptos_router::hooks::use_navigate;
|
||||||
use leptos_shadcn_skeleton::Skeleton;
|
use leptos_shadcn_skeleton::Skeleton;
|
||||||
use leptos_shadcn_toast::SonnerProvider;
|
use crate::components::toast::Toaster;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<SonnerProvider>
|
<InnerApp />
|
||||||
<InnerApp />
|
<Toaster />
|
||||||
</SonnerProvider>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn InnerApp() -> impl IntoView {
|
fn InnerApp() -> impl IntoView {
|
||||||
crate::store::provide_torrent_store();
|
crate::store::provide_torrent_store();
|
||||||
|
crate::components::toast::provide_toast_context();
|
||||||
let store = use_context::<crate::store::TorrentStore>();
|
let store = use_context::<crate::store::TorrentStore>();
|
||||||
|
|
||||||
let is_loading = signal(true);
|
let is_loading = signal(true);
|
||||||
@@ -31,6 +31,7 @@ fn InnerApp() -> impl IntoView {
|
|||||||
Effect::new(move |_| {
|
Effect::new(move |_| {
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
log::info!("App initialization started...");
|
log::info!("App initialization started...");
|
||||||
|
gloo_console::log!("APP INIT: Checking setup status...");
|
||||||
|
|
||||||
// Check if setup is needed via Server Function
|
// Check if setup is needed via Server Function
|
||||||
match shared::server_fns::auth::get_setup_status().await {
|
match shared::server_fns::auth::get_setup_status().await {
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ pub mod context_menu;
|
|||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod torrent;
|
pub mod torrent;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod toast;
|
||||||
|
|||||||
111
frontend/src/components/toast.rs
Normal file
111
frontend/src/components/toast.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use shared::NotificationLevel;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Toast {
|
||||||
|
pub id: String,
|
||||||
|
pub message: String,
|
||||||
|
pub level: NotificationLevel,
|
||||||
|
pub visible: RwSignal<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct ToastContext {
|
||||||
|
pub toasts: RwSignal<HashMap<String, Toast>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToastContext {
|
||||||
|
pub fn add(&self, message: impl Into<String>, level: NotificationLevel) {
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
let message = message.into();
|
||||||
|
let toast = Toast {
|
||||||
|
id: id.clone(),
|
||||||
|
message,
|
||||||
|
level,
|
||||||
|
visible: RwSignal::new(true),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.toasts.update(|m| {
|
||||||
|
m.insert(id.clone(), toast);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto remove after 5 seconds
|
||||||
|
let toasts = self.toasts;
|
||||||
|
let id_clone = id.clone();
|
||||||
|
leptos::task::spawn_local(async move {
|
||||||
|
gloo_timers::future::TimeoutFuture::new(5000).await;
|
||||||
|
toasts.update(|m| {
|
||||||
|
if let Some(t) = m.get(&id_clone) {
|
||||||
|
t.visible.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Wait for animation
|
||||||
|
gloo_timers::future::TimeoutFuture::new(300).await;
|
||||||
|
toasts.update(|m| {
|
||||||
|
m.remove(&id_clone);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn provide_toast_context() {
|
||||||
|
let toasts = RwSignal::new(HashMap::new());
|
||||||
|
provide_context(ToastContext { toasts });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn Toaster() -> impl IntoView {
|
||||||
|
let context = expect_context::<ToastContext>();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="fixed top-4 right-4 z-[100] flex flex-col gap-2 w-full max-w-sm pointer-events-none">
|
||||||
|
{move || {
|
||||||
|
context.toasts.get().into_values().map(|toast| {
|
||||||
|
view! { <ToastItem toast=toast /> }
|
||||||
|
}).collect::<Vec<_>>()
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ToastItem(toast: Toast) -> impl IntoView {
|
||||||
|
let (visible, set_visible) = (toast.visible, toast.visible.write_only());
|
||||||
|
|
||||||
|
let base_classes = "pointer-events-auto relative w-full rounded-lg border p-4 shadow-lg transition-all duration-300 ease-in-out";
|
||||||
|
let color_classes = match toast.level {
|
||||||
|
NotificationLevel::Success => "bg-green-50 text-green-900 border-green-200 dark:bg-green-900 dark:text-green-100 dark:border-green-800",
|
||||||
|
NotificationLevel::Error => "bg-red-50 text-red-900 border-red-200 dark:bg-red-900 dark:text-red-100 dark:border-red-800",
|
||||||
|
NotificationLevel::Warning => "bg-yellow-50 text-yellow-900 border-yellow-200 dark:bg-yellow-900 dark:text-yellow-100 dark:border-yellow-800",
|
||||||
|
NotificationLevel::Info => "bg-blue-50 text-blue-900 border-blue-200 dark:bg-blue-900 dark:text-blue-100 dark:border-blue-800",
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class=move || format!("{} {} {}",
|
||||||
|
base_classes,
|
||||||
|
color_classes,
|
||||||
|
if visible.get() { "opacity-100 translate-x-0" } else { "opacity-0 translate-x-full" }
|
||||||
|
)
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm font-medium">{toast.message.clone()}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="inline-flex shrink-0 opacity-50 hover:opacity-100 focus:opacity-100 focus:outline-none"
|
||||||
|
on:click=move |_| set_visible.set(false)
|
||||||
|
>
|
||||||
|
<span class="sr-only">"Kapat"</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
|
||||||
|
<line x1="18" x2="6" y1="6" y2="18"></line>
|
||||||
|
<line x1="6" x2="18" y1="6" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,13 +7,18 @@ use std::collections::HashMap;
|
|||||||
use struct_patch::traits::Patch;
|
use struct_patch::traits::Patch;
|
||||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||||
|
|
||||||
|
use crate::components::toast::ToastContext;
|
||||||
|
|
||||||
pub fn show_toast(level: NotificationLevel, message: impl Into<String>) {
|
pub fn show_toast(level: NotificationLevel, message: impl Into<String>) {
|
||||||
let msg = message.into();
|
let msg = message.into();
|
||||||
match level {
|
gloo_console::log!("TOAST CALL:", &msg, format!("{:?}", level));
|
||||||
NotificationLevel::Info => { leptos_shadcn_toast::toast::info(&msg).show(); },
|
log::info!("Displaying toast: [{:?}] {}", level, msg);
|
||||||
NotificationLevel::Success => { leptos_shadcn_toast::toast::success(&msg).show(); },
|
|
||||||
NotificationLevel::Warning => { leptos_shadcn_toast::toast::warning(&msg).show(); },
|
if let Some(context) = use_context::<ToastContext>() {
|
||||||
NotificationLevel::Error => { leptos_shadcn_toast::toast::error(&msg).show(); },
|
context.add(msg, level);
|
||||||
|
} else {
|
||||||
|
log::error!("ToastContext not found!");
|
||||||
|
gloo_console::error!("ToastContext not found!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,4 +26,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
require("tailwindcss-animate"),
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user