diff --git a/frontend/src/components/navbar.rs b/frontend/src/components/navbar.rs index 0712183..69dc48b 100644 --- a/frontend/src/components/navbar.rs +++ b/frontend/src/components/navbar.rs @@ -13,6 +13,7 @@ pub struct Props { #[function_component(Navbar)] pub fn navbar(props: &Props) -> Html { let route = use_route::(); + let sidebar_open = use_state(|| false); let link = |to: Route, label: &str| { let active = route.as_ref() == Some(&to); @@ -24,16 +25,56 @@ pub fn navbar(props: &Props) -> Html { let on_logout = { let cb = props.on_logout.clone(); + let sidebar_open = sidebar_open.clone(); Callback::from(move |e: MouseEvent| { e.prevent_default(); + sidebar_open.set(false); cb.emit(()); }) }; + let toggle = { + let sidebar_open = sidebar_open.clone(); + Callback::from(move |_: MouseEvent| { + sidebar_open.set(!*sidebar_open); + }) + }; + + let close_overlay = { + let sidebar_open = sidebar_open.clone(); + Callback::from(move |_: MouseEvent| { + sidebar_open.set(false); + }) + }; + + // Close sidebar when any nav link is clicked + let on_nav_click = { + let sidebar_open = sidebar_open.clone(); + Callback::from(move |_: MouseEvent| { + sidebar_open.set(false); + }) + }; + + let sidebar_class = if *sidebar_open { + "sidebar open" + } else { + "sidebar" + }; + let overlay_class = if *sidebar_open { + "sidebar-overlay open" + } else { + "sidebar-overlay" + }; + html! { - + } } diff --git a/frontend/src/pages/dashboard.rs b/frontend/src/pages/dashboard.rs index 5c91e57..59ab0f6 100644 --- a/frontend/src/pages/dashboard.rs +++ b/frontend/src/pages/dashboard.rs @@ -281,17 +281,26 @@ pub fn dashboard() -> Html { }; let scheduled_jobs_html = { - let next_pipeline = s.scheduled.as_ref().and_then(|sc| sc.next_pipeline.as_ref()); + let next_pipeline = s + .scheduled + .as_ref() + .and_then(|sc| sc.next_pipeline.as_ref()); let next_monitor = s.scheduled.as_ref().and_then(|sc| sc.next_monitor.as_ref()); - let pipeline_next_str = next_pipeline.map(|n| format_next_run(n)).unwrap_or_default(); + let pipeline_next_str = next_pipeline + .map(|n| format_next_run(n)) + .unwrap_or_default(); let monitor_next_str = next_monitor.map(|n| format_next_run(n)).unwrap_or_default(); - let pipeline_last = s.scheduler.as_ref() + let pipeline_last = s + .scheduler + .as_ref() .and_then(|sc| sc.get("pipeline")) .and_then(|j| j.get("last_result")) .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); - let monitor_last = s.scheduler.as_ref() + let monitor_last = s + .scheduler + .as_ref() .and_then(|sc| sc.get("monitor")) .and_then(|j| j.get("last_result")) .and_then(|v| v.as_str()) @@ -483,7 +492,7 @@ pub fn dashboard() -> Html { { "Query" }{ "Status" }{ "Error" } - { for s.queue.items.iter().map(|item| html! { + { for s.queue.items.iter().take(10).map(|item| html! { { &item.query } @@ -492,6 +501,11 @@ pub fn dashboard() -> Html { })} + if s.queue.items.len() > 10 { +

+ { format!("and {} more...", s.queue.items.len() - 10) } +

+ } } else if s.queue.pending > 0 || s.queue.downloading > 0 {
@@ -511,7 +525,7 @@ pub fn dashboard() -> Html { { "Title" }{ "Artist" }{ "Album" }{ "MBID" } - { for tagging.items.iter().map(|t| html! { + { for tagging.items.iter().take(10).map(|t| html! { { t.title.as_deref().unwrap_or("Unknown") } { t.artist.as_deref().unwrap_or("") } @@ -527,6 +541,11 @@ pub fn dashboard() -> Html { })} + if tagging.items.len() > 10 { +

+ { format!("and {} more...", tagging.items.len() - 10) } +

+ } }
} diff --git a/frontend/style.css b/frontend/style.css index dd01e0c..572ea72 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -344,3 +344,59 @@ tr[draggable="true"]:active { cursor: grabbing; } flex-direction: column; gap: 0.25rem; } + +/* Hamburger menu button — hidden on desktop */ +.hamburger { + display: none; + position: fixed; + top: 0.75rem; + left: 0.75rem; + z-index: 101; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: var(--radius); + color: var(--text-primary); + font-size: 1.4rem; + padding: 0.25rem 0.6rem; + cursor: pointer; + line-height: 1; +} + +/* Sidebar overlay — hidden on desktop */ +.sidebar-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 99; +} + +/* Mobile responsive */ +@media (max-width: 768px) { + .hamburger { display: block; } + .sidebar-overlay.open { display: block; } + .sidebar { + transform: translateX(-100%); + z-index: 100; + transition: transform 0.2s ease; + } + .sidebar.open { transform: translateX(0); } + .main-content { + margin-left: 0; + padding: 1rem; + padding-top: 3.5rem; + } + .stats-grid { grid-template-columns: repeat(2, 1fr); } + table { display: block; overflow-x: auto; } + .form-group input[type="text"], + .form-group input[type="number"], + .form-group input[type="password"], + .form-group select { + width: 100% !important; + min-width: 0 !important; + } + .tab-bar { overflow-x: auto; } + .album-art-lg { width: 120px; height: 120px; } + .album-header { flex-direction: column; } + .artist-photo { width: 80px; height: 80px; } +}