now responsive for mobile
This commit is contained in:
@@ -13,6 +13,7 @@ pub struct Props {
|
||||
#[function_component(Navbar)]
|
||||
pub fn navbar(props: &Props) -> Html {
|
||||
let route = use_route::<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! {
|
||||
<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>
|
||||
<nav>
|
||||
<nav onclick={on_nav_click}>
|
||||
{ link(Route::Dashboard, "Dashboard") }
|
||||
{ link(Route::Search, "Search") }
|
||||
{ link(Route::Library, "Library") }
|
||||
@@ -48,5 +89,6 @@ pub fn navbar(props: &Props) -> Html {
|
||||
<a href="#" class="text-sm" onclick={on_logout}>{ "Logout" }</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
<tr><th>{ "Query" }</th><th>{ "Status" }</th><th>{ "Error" }</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ for s.queue.items.iter().map(|item| html! {
|
||||
{ for s.queue.items.iter().take(10).map(|item| html! {
|
||||
<tr>
|
||||
<td>{ &item.query }</td>
|
||||
<td><StatusBadge status={item.status.clone()} /></td>
|
||||
@@ -492,6 +501,11 @@ pub fn dashboard() -> Html {
|
||||
})}
|
||||
</tbody>
|
||||
</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>
|
||||
} else if s.queue.pending > 0 || s.queue.downloading > 0 {
|
||||
<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>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ for tagging.items.iter().map(|t| html! {
|
||||
{ for tagging.items.iter().take(10).map(|t| html! {
|
||||
<tr>
|
||||
<td>{ t.title.as_deref().unwrap_or("Unknown") }</td>
|
||||
<td>{ t.artist.as_deref().unwrap_or("") }</td>
|
||||
@@ -527,6 +541,11 @@ pub fn dashboard() -> Html {
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
if tagging.items.len() > 10 {
|
||||
<p class="text-sm text-muted mt-1">
|
||||
{ format!("and {} more...", tagging.items.len() - 10) }
|
||||
</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user