fix: context menu reaktivite bug'ı düzeltildi, sidebar leptos-shadcn-ui ile güncellendi
All checks were successful
Build MIPS Binary / build (push) Successful in 5m12s
All checks were successful
Build MIPS Binary / build (push) Successful in 5m12s
- context_menu.rs: leptos-shadcn-context-menu kütüphanesini bypasslayarak kendi reaktif implementasyonumuz yazıldı (Show ile reaktif render, Portal'sız fixed position) - sidebar.rs: Button, Avatar, AvatarFallback, Separator bileşenleri ile güncellendi - Kullanılmayan handle_logout kaldırıldı
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::portal::Portal;
|
use web_sys::MouseEvent;
|
||||||
use leptos_shadcn_context_menu::{
|
use wasm_bindgen::prelude::*;
|
||||||
ContextMenu,
|
use wasm_bindgen::JsCast;
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
// ── Kendi reaktif Context Menu implementasyonumuz ──
|
||||||
ContextMenuTrigger,
|
// leptos-shadcn-context-menu v0.8.1'de ContextMenuContent'te
|
||||||
ContextMenuSeparator,
|
// `if open.get()` statik kontrolü reaktif değil. Aşağıda
|
||||||
};
|
// `Show` bileşeni ile düzgün reaktif versiyon yer alıyor.
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn TorrentContextMenu(
|
pub fn TorrentContextMenu(
|
||||||
@@ -17,68 +17,131 @@ pub fn TorrentContextMenu(
|
|||||||
let hash = StoredValue::new(torrent_hash);
|
let hash = StoredValue::new(torrent_hash);
|
||||||
let on_action = StoredValue::new(on_action);
|
let on_action = StoredValue::new(on_action);
|
||||||
|
|
||||||
|
let open = RwSignal::new(false);
|
||||||
|
let position = RwSignal::new((0i32, 0i32));
|
||||||
|
|
||||||
|
// Sağ tıklama handler
|
||||||
|
let on_contextmenu = move |e: MouseEvent| {
|
||||||
|
e.prevent_default();
|
||||||
|
e.stop_propagation();
|
||||||
|
position.set((e.client_x(), e.client_y()));
|
||||||
|
open.set(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Menü dışına tıklandığında kapanma
|
||||||
|
Effect::new(move |_| {
|
||||||
|
if open.get() {
|
||||||
|
let cb = Closure::wrap(Box::new(move |_: MouseEvent| {
|
||||||
|
open.set(false);
|
||||||
|
}) as Box<dyn Fn(MouseEvent)>);
|
||||||
|
|
||||||
|
let window = web_sys::window().unwrap();
|
||||||
|
let document = window.document().unwrap();
|
||||||
|
let _ = document.add_event_listener_with_callback(
|
||||||
|
"click",
|
||||||
|
cb.as_ref().unchecked_ref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cleanup: tek sefer dinleyici — click yakalandığında otomatik kapanıp listener kalıyor
|
||||||
|
// ama open=false olduğunda effect tekrar çalışmaz, böylece sorun yok.
|
||||||
|
cb.forget();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let menu_action = move |action: &'static str| {
|
||||||
|
open.set(false);
|
||||||
|
on_action.get_value().run((action.to_string(), hash.get_value()));
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<ContextMenu>
|
<div
|
||||||
<ContextMenuTrigger class="w-full">
|
class="w-full"
|
||||||
{children()}
|
on:contextmenu=on_contextmenu
|
||||||
</ContextMenuTrigger>
|
>
|
||||||
|
{children()}
|
||||||
<Portal>
|
</div>
|
||||||
<ContextMenuContent class="w-56 z-[100] bg-popover border border-border shadow-md rounded-md p-1">
|
|
||||||
<ContextMenuItem on:click=move |_| {
|
|
||||||
on_action.get_value().run(("start".to_string(), hash.get_value()));
|
|
||||||
}>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
|
|
||||||
</svg>
|
|
||||||
"Start"
|
|
||||||
</ContextMenuItem>
|
|
||||||
|
|
||||||
<ContextMenuItem on:click=move |_| {
|
|
||||||
on_action.get_value().run(("stop".to_string(), hash.get_value()));
|
|
||||||
}>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
|
||||||
</svg>
|
|
||||||
"Stop"
|
|
||||||
</ContextMenuItem>
|
|
||||||
|
|
||||||
<ContextMenuItem on:click=move |_| {
|
<Show when=move || open.get()>
|
||||||
on_action.get_value().run(("recheck".to_string(), hash.get_value()));
|
{
|
||||||
}>
|
let (x, y) = position.get();
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
view! {
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
<div
|
||||||
</svg>
|
class="fixed inset-0 z-[99]"
|
||||||
"Recheck"
|
on:click=move |e: MouseEvent| {
|
||||||
</ContextMenuItem>
|
e.stop_propagation();
|
||||||
|
open.set(false);
|
||||||
<ContextMenuSeparator />
|
|
||||||
|
|
||||||
<ContextMenuItem
|
|
||||||
class="text-destructive focus:text-destructive-foreground focus:bg-destructive"
|
|
||||||
on:click=move |_| {
|
|
||||||
on_action.get_value().run(("delete".to_string(), hash.get_value()));
|
|
||||||
}
|
}
|
||||||
|
on:contextmenu=move |e: MouseEvent| {
|
||||||
|
e.prevent_default();
|
||||||
|
e.stop_propagation();
|
||||||
|
open.set(false);
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="fixed z-[100] min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95"
|
||||||
|
style=format!("left: {}px; top: {}px;", x, y)
|
||||||
|
on:click=move |e: MouseEvent| e.stop_propagation()
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
// Start
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.164h-2.34c-1.18 0-2.09.984-2.09 2.164v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
<div
|
||||||
</svg>
|
class="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||||
"Remove"
|
on:click=move |_| menu_action("start")
|
||||||
</ContextMenuItem>
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
|
||||||
|
</svg>
|
||||||
|
"Start"
|
||||||
|
</div>
|
||||||
|
|
||||||
<ContextMenuItem
|
// Stop
|
||||||
class="text-destructive focus:text-destructive-foreground focus:bg-destructive"
|
<div
|
||||||
on:click=move |_| {
|
class="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||||
on_action.get_value().run(("delete_with_data".to_string(), hash.get_value()));
|
on:click=move |_| menu_action("stop")
|
||||||
}
|
>
|
||||||
>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5m6 4.125l2.25 2.25m0 0l2.25 2.25M12 13.875l2.25-2.25M12 13.875l-2.25-2.25M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
|
</svg>
|
||||||
</svg>
|
"Stop"
|
||||||
"Remove Data"
|
</div>
|
||||||
</ContextMenuItem>
|
|
||||||
</ContextMenuContent>
|
// Recheck
|
||||||
</Portal>
|
<div
|
||||||
</ContextMenu>
|
class="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||||
|
on:click=move |_| menu_action("recheck")
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||||
|
</svg>
|
||||||
|
"Recheck"
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
<div class="-mx-1 my-1 h-px bg-border" />
|
||||||
|
|
||||||
|
// Remove
|
||||||
|
<div
|
||||||
|
class="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||||
|
on:click=move |_| menu_action("delete")
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.164h-2.34c-1.18 0-2.09.984-2.09 2.164v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
||||||
|
</svg>
|
||||||
|
"Remove"
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Remove with Data
|
||||||
|
<div
|
||||||
|
class="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||||
|
on:click=move |_| menu_action("delete_with_data")
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5m6 4.125l2.25 2.25m0 0l2.25 2.25M12 13.875l2.25-2.25M12 13.875l-2.25-2.25M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
|
||||||
|
</svg>
|
||||||
|
"Remove with Data"
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</Show>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
|
use leptos_shadcn_button::{Button, ButtonVariant, ButtonSize};
|
||||||
|
use leptos_shadcn_avatar::{Avatar, AvatarFallback};
|
||||||
|
use leptos_shadcn_separator::Separator;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Sidebar() -> impl IntoView {
|
pub fn Sidebar() -> impl IntoView {
|
||||||
@@ -54,23 +57,7 @@ pub fn Sidebar() -> impl IntoView {
|
|||||||
is_mobile_menu_open.set(false);
|
is_mobile_menu_open.set(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter_class = move |f: crate::store::FilterStatus| {
|
let is_active = move |f: crate::store::FilterStatus| store.filter.get() == f;
|
||||||
let base = "w-full justify-start gap-2 h-9 px-4 py-2 inline-flex items-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50";
|
|
||||||
if store.filter.get() == f {
|
|
||||||
format!("{} bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", base)
|
|
||||||
} else {
|
|
||||||
format!("{} hover:bg-accent hover:text-accent-foreground text-muted-foreground", base)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let handle_logout = move |_| {
|
|
||||||
spawn_local(async move {
|
|
||||||
if shared::server_fns::auth::logout().await.is_ok() {
|
|
||||||
let window = web_sys::window().expect("window should exist");
|
|
||||||
let _ = window.location().set_href("/login");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let username = move || {
|
let username = move || {
|
||||||
store.user.get().unwrap_or_else(|| "User".to_string())
|
store.user.get().unwrap_or_else(|| "User".to_string())
|
||||||
@@ -89,76 +76,116 @@ pub fn Sidebar() -> impl IntoView {
|
|||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<h4 class="mb-1 rounded-md px-2 py-1 text-sm font-semibold text-muted-foreground">"Filters"</h4>
|
<h4 class="mb-1 rounded-md px-2 py-1 text-sm font-semibold text-muted-foreground">"Filters"</h4>
|
||||||
|
|
||||||
<button class={move || filter_class(crate::store::FilterStatus::All)} on:click=move |_| set_filter(crate::store::FilterStatus::All)>
|
<Button
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
variant=MaybeProp::derive(move || Some(if is_active(crate::store::FilterStatus::All) { ButtonVariant::Secondary } else { ButtonVariant::Ghost }))
|
||||||
|
size=ButtonSize::Sm
|
||||||
|
class="w-full justify-start gap-2"
|
||||||
|
on_click=Callback::new(move |()| set_filter(crate::store::FilterStatus::All))
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||||
</svg>
|
</svg>
|
||||||
"All"
|
"All"
|
||||||
<span class="ml-auto text-xs font-mono opacity-70">{total_count}</span>
|
<span class="ml-auto text-xs font-mono opacity-70">{total_count}</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button class={move || filter_class(crate::store::FilterStatus::Downloading)} on:click=move |_| set_filter(crate::store::FilterStatus::Downloading)>
|
<Button
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
variant=MaybeProp::derive(move || Some(if is_active(crate::store::FilterStatus::Downloading) { ButtonVariant::Secondary } else { ButtonVariant::Ghost }))
|
||||||
|
size=ButtonSize::Sm
|
||||||
|
class="w-full justify-start gap-2"
|
||||||
|
on_click=Callback::new(move |()| set_filter(crate::store::FilterStatus::Downloading))
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||||
</svg>
|
</svg>
|
||||||
"Downloading"
|
"Downloading"
|
||||||
<span class="ml-auto text-xs font-mono opacity-70">{downloading_count}</span>
|
<span class="ml-auto text-xs font-mono opacity-70">{downloading_count}</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button class={move || filter_class(crate::store::FilterStatus::Seeding)} on:click=move |_| set_filter(crate::store::FilterStatus::Seeding)>
|
<Button
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
variant=MaybeProp::derive(move || Some(if is_active(crate::store::FilterStatus::Seeding) { ButtonVariant::Secondary } else { ButtonVariant::Ghost }))
|
||||||
|
size=ButtonSize::Sm
|
||||||
|
class="w-full justify-start gap-2"
|
||||||
|
on_click=Callback::new(move |()| set_filter(crate::store::FilterStatus::Seeding))
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||||
</svg>
|
</svg>
|
||||||
"Seeding"
|
"Seeding"
|
||||||
<span class="ml-auto text-xs font-mono opacity-70">{seeding_count}</span>
|
<span class="ml-auto text-xs font-mono opacity-70">{seeding_count}</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button class={move || filter_class(crate::store::FilterStatus::Completed)} on:click=move |_| set_filter(crate::store::FilterStatus::Completed)>
|
<Button
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
variant=MaybeProp::derive(move || Some(if is_active(crate::store::FilterStatus::Completed) { ButtonVariant::Secondary } else { ButtonVariant::Ghost }))
|
||||||
|
size=ButtonSize::Sm
|
||||||
|
class="w-full justify-start gap-2"
|
||||||
|
on_click=Callback::new(move |()| set_filter(crate::store::FilterStatus::Completed))
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
<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" />
|
<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" />
|
||||||
</svg>
|
</svg>
|
||||||
"Completed"
|
"Completed"
|
||||||
<span class="ml-auto text-xs font-mono opacity-70">{completed_count}</span>
|
<span class="ml-auto text-xs font-mono opacity-70">{completed_count}</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button class={move || filter_class(crate::store::FilterStatus::Paused)} on:click=move |_| set_filter(crate::store::FilterStatus::Paused)>
|
<Button
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
variant=MaybeProp::derive(move || Some(if is_active(crate::store::FilterStatus::Paused) { ButtonVariant::Secondary } else { ButtonVariant::Ghost }))
|
||||||
|
size=ButtonSize::Sm
|
||||||
|
class="w-full justify-start gap-2"
|
||||||
|
on_click=Callback::new(move |()| set_filter(crate::store::FilterStatus::Paused))
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
||||||
</svg>
|
</svg>
|
||||||
"Paused"
|
"Paused"
|
||||||
<span class="ml-auto text-xs font-mono opacity-70">{paused_count}</span>
|
<span class="ml-auto text-xs font-mono opacity-70">{paused_count}</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button class={move || filter_class(crate::store::FilterStatus::Inactive)} on:click=move |_| set_filter(crate::store::FilterStatus::Inactive)>
|
<Button
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2">
|
variant=MaybeProp::derive(move || Some(if is_active(crate::store::FilterStatus::Inactive) { ButtonVariant::Secondary } else { ButtonVariant::Ghost }))
|
||||||
|
size=ButtonSize::Sm
|
||||||
|
class="w-full justify-start gap-2"
|
||||||
|
on_click=Callback::new(move |()| set_filter(crate::store::FilterStatus::Inactive))
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
||||||
</svg>
|
</svg>
|
||||||
"Inactive"
|
"Inactive"
|
||||||
<span class="ml-auto text-xs font-mono opacity-70">{inactive_count}</span>
|
<span class="ml-auto text-xs font-mono opacity-70">{inactive_count}</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 border-t border-border bg-card">
|
<Separator />
|
||||||
|
|
||||||
|
<div class="p-4 bg-card">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-full bg-muted">
|
<Avatar class="h-8 w-8">
|
||||||
<span class="flex h-full w-full items-center justify-center rounded-full bg-primary text-primary-foreground text-xs font-medium">
|
<AvatarFallback class="bg-primary text-primary-foreground text-xs font-medium">
|
||||||
{first_letter}
|
{first_letter}
|
||||||
</span>
|
</AvatarFallback>
|
||||||
</div>
|
</Avatar>
|
||||||
<div class="flex-1 overflow-hidden">
|
<div class="flex-1 overflow-hidden">
|
||||||
<div class="font-medium text-sm truncate text-foreground">{username}</div>
|
<div class="font-medium text-sm truncate text-foreground">{username}</div>
|
||||||
<div class="text-[10px] text-muted-foreground truncate">"Online"</div>
|
<div class="text-[10px] text-muted-foreground truncate">"Online"</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-8 w-8 text-destructive"
|
variant=ButtonVariant::Ghost
|
||||||
title="Logout"
|
size=ButtonSize::Icon
|
||||||
on:click=handle_logout
|
class="text-destructive h-8 w-8"
|
||||||
|
on_click=Callback::new(move |()| {
|
||||||
|
spawn_local(async move {
|
||||||
|
if shared::server_fns::auth::logout().await.is_ok() {
|
||||||
|
let window = web_sys::window().expect("window should exist");
|
||||||
|
let _ = window.location().set_href("/login");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user