feat: implement professional Sidenav layout and mobile Sheet menu
Some checks failed
Build MIPS Binary / build (push) Failing after 1m29s
Some checks failed
Build MIPS Binary / build (push) Failing after 1m29s
This commit is contained in:
@@ -2,51 +2,30 @@ use leptos::prelude::*;
|
||||
use crate::components::layout::sidebar::Sidebar;
|
||||
use crate::components::layout::toolbar::Toolbar;
|
||||
use crate::components::layout::statusbar::StatusBar;
|
||||
use crate::components::ui::sidenav::{SidenavWrapper, Sidenav, SidenavInset};
|
||||
|
||||
#[component]
|
||||
pub fn Protected(children: Children) -> impl IntoView {
|
||||
// Mobil menü durumu için bir sinyal oluşturuyoruz (RwSignal for easier passing)
|
||||
let is_mobile_menu_open = RwSignal::new(false);
|
||||
|
||||
// Sinyali context olarak sağlıyoruz ki Toolbar ve Sidebar buna erişebilsin
|
||||
provide_context(is_mobile_menu_open);
|
||||
|
||||
view! {
|
||||
<div class="flex h-screen w-full overflow-hidden bg-background">
|
||||
|
||||
// --- SIDEBAR (Desktop: Sabit, Mobil: Overlay) ---
|
||||
<aside class=move || {
|
||||
let base = "fixed inset-y-0 left-0 z-50 w-64 transform transition-transform duration-300 ease-in-out border-r border-border bg-card lg:relative lg:translate-x-0";
|
||||
if is_mobile_menu_open.get() {
|
||||
format!("{} translate-x-0", base)
|
||||
} else {
|
||||
format!("{} -translate-x-full", base)
|
||||
}
|
||||
}>
|
||||
<SidenavWrapper attr:style="--sidenav-width:16rem; --sidenav-width-icon:3rem;">
|
||||
// Masaüstü Sidenav
|
||||
<Sidenav>
|
||||
<Sidebar />
|
||||
</aside>
|
||||
</Sidenav>
|
||||
|
||||
// Mobil arka plan karartma (Overlay)
|
||||
<Show when=move || is_mobile_menu_open.get()>
|
||||
<div
|
||||
class="fixed inset-0 z-40 bg-background/80 backdrop-blur-sm lg:hidden"
|
||||
on:click=move |_| is_mobile_menu_open.set(false)
|
||||
></div>
|
||||
</Show>
|
||||
|
||||
// --- MAIN CONTENT AREA ---
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
// --- TOOLBAR (TOP) ---
|
||||
// İçerik Alanı
|
||||
<SidenavInset class="flex flex-col h-screen overflow-hidden">
|
||||
// Toolbar (Üst Bar)
|
||||
<Toolbar />
|
||||
|
||||
// --- MAIN CONTENT ---
|
||||
// Ana İçerik
|
||||
<main class="flex-1 overflow-hidden relative bg-background">
|
||||
{children()}
|
||||
</main>
|
||||
|
||||
// --- STATUS BAR (BOTTOM) ---
|
||||
// Alt Bar
|
||||
<StatusBar />
|
||||
</div>
|
||||
</div>
|
||||
</SidenavInset>
|
||||
</SidenavWrapper>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos::task::spawn_local;
|
||||
use crate::components::ui::sidenav::*;
|
||||
use crate::components::ui::button::{Button, ButtonVariant, ButtonSize};
|
||||
use crate::components::ui::theme_toggle::ThemeToggle;
|
||||
|
||||
#[component]
|
||||
pub fn Sidebar() -> impl IntoView {
|
||||
let store = use_context::<crate::store::TorrentStore>().expect("store not provided");
|
||||
let is_mobile_menu_open = use_context::<RwSignal<bool>>().expect("mobile menu state not provided");
|
||||
|
||||
let total_count = move || store.torrents.with(|map| map.len());
|
||||
let downloading_count = move || {
|
||||
@@ -52,7 +53,6 @@ pub fn Sidebar() -> impl IntoView {
|
||||
|
||||
let set_filter = move |f: crate::store::FilterStatus| {
|
||||
store.filter.set(f);
|
||||
is_mobile_menu_open.set(false);
|
||||
};
|
||||
|
||||
let is_active = move |f: crate::store::FilterStatus| store.filter.get() == f;
|
||||
@@ -66,83 +66,89 @@ pub fn Sidebar() -> impl IntoView {
|
||||
};
|
||||
|
||||
view! {
|
||||
<div class="w-full h-full flex flex-col bg-card" style="padding-top: env(safe-area-inset-top);">
|
||||
<div class="p-4 flex-1 overflow-y-auto">
|
||||
<div class="mb-4 px-2 text-lg font-semibold tracking-tight text-foreground">
|
||||
"VibeTorrent"
|
||||
<SidenavHeader>
|
||||
<div class="flex items-center gap-2 px-2 py-4">
|
||||
<div class="flex size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.362 5.214A8.252 8.252 0 0112 21 8.25 8.25 0 016.038 7.048 8.287 8.287 0 009 9.6a8.983 8.983 0 013.361-6.867 8.21 8.25 0 003 2.48z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<h4 class="mb-1 rounded-md px-2 py-1 text-sm font-semibold text-muted-foreground">"Filters"</h4>
|
||||
|
||||
<SidebarButton
|
||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::All))
|
||||
on_click=move |_| set_filter(crate::store::FilterStatus::All)
|
||||
icon="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||
label="All"
|
||||
count=Signal::derive(total_count)
|
||||
/>
|
||||
<SidebarButton
|
||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Downloading))
|
||||
on_click=move |_| set_filter(crate::store::FilterStatus::Downloading)
|
||||
icon="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"
|
||||
label="Downloading"
|
||||
count=Signal::derive(downloading_count)
|
||||
/>
|
||||
<SidebarButton
|
||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Seeding))
|
||||
on_click=move |_| set_filter(crate::store::FilterStatus::Seeding)
|
||||
icon="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"
|
||||
label="Seeding"
|
||||
count=Signal::derive(seeding_count)
|
||||
/>
|
||||
<SidebarButton
|
||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Completed))
|
||||
on_click=move |_| set_filter(crate::store::FilterStatus::Completed)
|
||||
icon="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
label="Completed"
|
||||
count=Signal::derive(completed_count)
|
||||
/>
|
||||
<SidebarButton
|
||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Paused))
|
||||
on_click=move |_| set_filter(crate::store::FilterStatus::Paused)
|
||||
icon="M15.75 5.25v13.5m-7.5-13.5v13.5"
|
||||
label="Paused"
|
||||
count=Signal::derive(paused_count)
|
||||
/>
|
||||
<SidebarButton
|
||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Inactive))
|
||||
on_click=move |_| set_filter(crate::store::FilterStatus::Inactive)
|
||||
icon="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"
|
||||
label="Inactive"
|
||||
count=Signal::derive(inactive_count)
|
||||
/>
|
||||
<div class="grid flex-1 text-left text-sm leading-tight overflow-hidden">
|
||||
<span class="truncate font-semibold text-foreground text-base">"VibeTorrent"</span>
|
||||
<span class="truncate text-[10px] text-muted-foreground opacity-70">"v3.0.0"</span>
|
||||
</div>
|
||||
</div>
|
||||
</SidenavHeader>
|
||||
|
||||
// Separator
|
||||
<div class="border-t border-border" />
|
||||
<SidenavContent>
|
||||
<SidenavGroup>
|
||||
<SidenavGroupLabel>"Filtreler"</SidenavGroupLabel>
|
||||
<SidenavGroupContent>
|
||||
<SidenavMenu>
|
||||
<SidebarItem
|
||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::All))
|
||||
on_click=move |_| set_filter(crate::store::FilterStatus::All)
|
||||
icon="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||
label="Tümü"
|
||||
count=Signal::derive(total_count)
|
||||
/>
|
||||
<SidebarItem
|
||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Downloading))
|
||||
on_click=move |_| set_filter(crate::store::FilterStatus::Downloading)
|
||||
icon="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"
|
||||
label="İndirilenler"
|
||||
count=Signal::derive(downloading_count)
|
||||
/>
|
||||
<SidebarItem
|
||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Seeding))
|
||||
on_click=move |_| set_filter(crate::store::FilterStatus::Seeding)
|
||||
icon="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"
|
||||
label="Gönderilenler"
|
||||
count=Signal::derive(seeding_count)
|
||||
/>
|
||||
<SidebarItem
|
||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Completed))
|
||||
on_click=move |_| set_filter(crate::store::FilterStatus::Completed)
|
||||
icon="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
label="Tamamlananlar"
|
||||
count=Signal::derive(completed_count)
|
||||
/>
|
||||
<SidebarItem
|
||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Paused))
|
||||
on_click=move |_| set_filter(crate::store::FilterStatus::Paused)
|
||||
icon="M15.75 5.25v13.5m-7.5-13.5v13.5"
|
||||
label="Durdurulanlar"
|
||||
count=Signal::derive(paused_count)
|
||||
/>
|
||||
<SidebarItem
|
||||
active=Signal::derive(move || is_active(crate::store::FilterStatus::Inactive))
|
||||
on_click=move |_| set_filter(crate::store::FilterStatus::Inactive)
|
||||
icon="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"
|
||||
label="Pasif"
|
||||
count=Signal::derive(inactive_count)
|
||||
/>
|
||||
</SidenavMenu>
|
||||
</SidenavGroupContent>
|
||||
</SidenavGroup>
|
||||
</SidenavContent>
|
||||
|
||||
<div class="p-4 bg-card" style="padding-bottom: calc(1rem + env(safe-area-inset-bottom));">
|
||||
<div class="flex items-center gap-3">
|
||||
// Avatar
|
||||
<div class="h-8 w-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-medium shrink-0">
|
||||
{first_letter}
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div class="font-medium text-sm truncate text-foreground">{username}</div>
|
||||
<div class="text-[10px] text-muted-foreground truncate">"Online"</div>
|
||||
</div>
|
||||
|
||||
// Theme toggle button
|
||||
<div class="inline-flex items-center justify-center size-8 rounded-md hover:bg-accent hover:text-accent-foreground text-muted-foreground hover:text-foreground transition-colors">
|
||||
<crate::components::ui::theme_toggle::ThemeToggle />
|
||||
</div>
|
||||
// Logout button
|
||||
<SidenavFooter>
|
||||
<div class="flex items-center gap-3 p-2 rounded-lg border bg-muted/30 shadow-xs overflow-hidden">
|
||||
<div class="h-8 w-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-xs font-medium shrink-0 border border-primary-foreground/10">
|
||||
{first_letter}
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div class="font-medium text-[11px] truncate text-foreground leading-tight">{username}</div>
|
||||
<div class="text-[9px] text-muted-foreground truncate opacity-70">"Yönetici"</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<ThemeToggle />
|
||||
|
||||
<Button
|
||||
variant=ButtonVariant::Ghost
|
||||
size=ButtonSize::Icon
|
||||
class="text-destructive hover:bg-destructive/10"
|
||||
attr:disabled=move || false
|
||||
class="size-7 text-destructive hover:bg-destructive/10"
|
||||
on:click=move |_| {
|
||||
spawn_local(async move {
|
||||
if shared::server_fns::auth::logout().await.is_ok() {
|
||||
@@ -152,37 +158,40 @@ pub fn Sidebar() -> impl IntoView {
|
||||
});
|
||||
}
|
||||
>
|
||||
<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="size-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" />
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidenavFooter>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn SidebarButton(
|
||||
fn SidebarItem(
|
||||
active: Signal<bool>,
|
||||
on_click: impl Fn(web_sys::MouseEvent) + 'static,
|
||||
on_click: impl Fn(web_sys::MouseEvent) + 'static + Send,
|
||||
#[prop(into)] icon: String,
|
||||
#[prop(into)] label: &'static str,
|
||||
count: Signal<usize>,
|
||||
) -> impl IntoView {
|
||||
let variant = move || if active.get() { ButtonVariant::Secondary } else { ButtonVariant::Ghost };
|
||||
|
||||
let variant = move || if active.get() { SidenavMenuButtonVariant::Outline } else { SidenavMenuButtonVariant::Default };
|
||||
let class = move || if active.get() { "bg-accent/50 border-accent text-foreground".to_string() } else { "text-muted-foreground hover:text-foreground".to_string() };
|
||||
|
||||
view! {
|
||||
<Button
|
||||
variant=Signal::derive(variant)
|
||||
class="justify-start gap-2 w-full h-8 px-3"
|
||||
on:click=on_click
|
||||
>
|
||||
<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=icon.clone() />
|
||||
</svg>
|
||||
{label}
|
||||
<span class="ml-auto text-xs font-mono opacity-70">{count}</span>
|
||||
</Button>
|
||||
<SidenavMenuItem>
|
||||
<SidenavMenuButton
|
||||
variant=Signal::derive(variant)
|
||||
class=Signal::derive(class)
|
||||
on:click=on_click
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 shrink-0">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d=icon.clone() />
|
||||
</svg>
|
||||
<span class="flex-1 truncate">{label}</span>
|
||||
<span class="text-[10px] font-mono opacity-50">{count}</span>
|
||||
</SidenavMenuButton>
|
||||
</SidenavMenuItem>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
use leptos::prelude::*;
|
||||
use icons::PanelLeft;
|
||||
use crate::components::torrent::add_torrent::AddTorrentDialog;
|
||||
use crate::components::ui::button::{Button};
|
||||
use crate::components::ui::button::{Button, ButtonVariant, ButtonSize};
|
||||
use crate::components::ui::sheet::{Sheet, SheetContent, SheetTrigger, SheetDirection};
|
||||
use crate::components::layout::sidebar::Sidebar;
|
||||
|
||||
#[component]
|
||||
pub fn Toolbar() -> impl IntoView {
|
||||
let show_add_modal = signal(false);
|
||||
let is_mobile_menu_open = use_context::<RwSignal<bool>>().expect("mobile menu state not provided");
|
||||
|
||||
view! {
|
||||
<div class="flex min-h-14 h-auto items-center border-b border-border bg-background px-4" style="padding-top: env(safe-area-inset-top);">
|
||||
// Sol kısım: Menü butonu + Add Torrent
|
||||
// Sol kısım: Menü butonu (Mobil) + Add Torrent
|
||||
<div class="flex items-center gap-3">
|
||||
// Mobile Menu Trigger
|
||||
<button
|
||||
class="inline-flex items-center justify-center size-9 rounded-md hover:bg-accent hover:text-accent-foreground lg:hidden"
|
||||
on:click=move |_| is_mobile_menu_open.update(|v| *v = !*v)
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-5 h-5 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
||||
</button>
|
||||
|
||||
// --- MOBILE SHEET (SIDEBAR) ---
|
||||
<div class="lg:hidden">
|
||||
<Sheet>
|
||||
<SheetTrigger variant=ButtonVariant::Ghost size=ButtonSize::Icon class="size-9">
|
||||
<PanelLeft class="size-5" />
|
||||
<span class="hidden">"Menüyü Aç"</span>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
direction=SheetDirection::Left
|
||||
class="p-0 w-[18rem] bg-card border-r border-border"
|
||||
hide_close_button=true
|
||||
>
|
||||
<div class="flex flex-col h-full overflow-hidden">
|
||||
<Sidebar />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
on:click=move |_| show_add_modal.1.set(true)
|
||||
@@ -31,7 +45,7 @@ pub fn Toolbar() -> impl IntoView {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
// Sağ kısım boşaltıldı (arama kutusu kaldırıldı)
|
||||
// Sağ kısım boş
|
||||
<div class="flex flex-1 items-center justify-end gap-2">
|
||||
</div>
|
||||
|
||||
@@ -40,4 +54,4 @@ pub fn Toolbar() -> impl IntoView {
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user