now responsive for mobile

This commit is contained in:
Connor Johnstone
2026-03-24 12:01:00 -04:00
parent 7c30f288cd
commit 29e6494e11
3 changed files with 125 additions and 8 deletions

View File

@@ -13,6 +13,7 @@ pub struct Props {
#[function_component(Navbar)] #[function_component(Navbar)]
pub fn navbar(props: &Props) -> Html { pub fn navbar(props: &Props) -> Html {
let route = use_route::<Route>(); let route = use_route::<Route>();
let sidebar_open = use_state(|| false);
let link = |to: Route, label: &str| { let link = |to: Route, label: &str| {
let active = route.as_ref() == Some(&to); let active = route.as_ref() == Some(&to);
@@ -24,16 +25,56 @@ pub fn navbar(props: &Props) -> Html {
let on_logout = { let on_logout = {
let cb = props.on_logout.clone(); let cb = props.on_logout.clone();
let sidebar_open = sidebar_open.clone();
Callback::from(move |e: MouseEvent| { Callback::from(move |e: MouseEvent| {
e.prevent_default(); e.prevent_default();
sidebar_open.set(false);
cb.emit(()); 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! { html! {
<div class="sidebar"> <>
<button class="hamburger" onclick={toggle}>
{ "\u{2630}" }
</button>
<div class={overlay_class} onclick={close_overlay}></div>
<div class={sidebar_class}>
<h1>{ "Shanty" }</h1> <h1>{ "Shanty" }</h1>
<nav> <nav onclick={on_nav_click}>
{ link(Route::Dashboard, "Dashboard") } { link(Route::Dashboard, "Dashboard") }
{ link(Route::Search, "Search") } { link(Route::Search, "Search") }
{ link(Route::Library, "Library") } { link(Route::Library, "Library") }
@@ -48,5 +89,6 @@ pub fn navbar(props: &Props) -> Html {
<a href="#" class="text-sm" onclick={on_logout}>{ "Logout" }</a> <a href="#" class="text-sm" onclick={on_logout}>{ "Logout" }</a>
</div> </div>
</div> </div>
</>
} }
} }

View File

@@ -281,17 +281,26 @@ pub fn dashboard() -> Html {
}; };
let scheduled_jobs_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 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 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(|sc| sc.get("pipeline"))
.and_then(|j| j.get("last_result")) .and_then(|j| j.get("last_result"))
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("") .unwrap_or("")
.to_string(); .to_string();
let monitor_last = s.scheduler.as_ref() let monitor_last = s
.scheduler
.as_ref()
.and_then(|sc| sc.get("monitor")) .and_then(|sc| sc.get("monitor"))
.and_then(|j| j.get("last_result")) .and_then(|j| j.get("last_result"))
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
@@ -483,7 +492,7 @@ pub fn dashboard() -> Html {
<tr><th>{ "Query" }</th><th>{ "Status" }</th><th>{ "Error" }</th></tr> <tr><th>{ "Query" }</th><th>{ "Status" }</th><th>{ "Error" }</th></tr>
</thead> </thead>
<tbody> <tbody>
{ for s.queue.items.iter().map(|item| html! { { for s.queue.items.iter().take(10).map(|item| html! {
<tr> <tr>
<td>{ &item.query }</td> <td>{ &item.query }</td>
<td><StatusBadge status={item.status.clone()} /></td> <td><StatusBadge status={item.status.clone()} /></td>
@@ -492,6 +501,11 @@ pub fn dashboard() -> Html {
})} })}
</tbody> </tbody>
</table> </table>
if s.queue.items.len() > 10 {
<p class="text-sm text-muted mt-1">
{ format!("and {} more...", s.queue.items.len() - 10) }
</p>
}
</div> </div>
} else if s.queue.pending > 0 || s.queue.downloading > 0 { } else if s.queue.pending > 0 || s.queue.downloading > 0 {
<div class="card"> <div class="card">
@@ -511,7 +525,7 @@ pub fn dashboard() -> Html {
<tr><th>{ "Title" }</th><th>{ "Artist" }</th><th>{ "Album" }</th><th>{ "MBID" }</th></tr> <tr><th>{ "Title" }</th><th>{ "Artist" }</th><th>{ "Album" }</th><th>{ "MBID" }</th></tr>
</thead> </thead>
<tbody> <tbody>
{ for tagging.items.iter().map(|t| html! { { for tagging.items.iter().take(10).map(|t| html! {
<tr> <tr>
<td>{ t.title.as_deref().unwrap_or("Unknown") }</td> <td>{ t.title.as_deref().unwrap_or("Unknown") }</td>
<td>{ t.artist.as_deref().unwrap_or("") }</td> <td>{ t.artist.as_deref().unwrap_or("") }</td>
@@ -527,6 +541,11 @@ pub fn dashboard() -> Html {
})} })}
</tbody> </tbody>
</table> </table>
if tagging.items.len() > 10 {
<p class="text-sm text-muted mt-1">
{ format!("and {} more...", tagging.items.len() - 10) }
</p>
}
} }
</div> </div>
} }

View File

@@ -344,3 +344,59 @@ tr[draggable="true"]:active { cursor: grabbing; }
flex-direction: column; flex-direction: column;
gap: 0.25rem; 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; }
}