now responsive for mobile
This commit is contained in:
@@ -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>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user