feat: move mobile slot tools into menu

Brand the mobile header as Foodsharing Manager and move refresh and notification
actions from the dashboard into the contextual navigation menu.
This commit is contained in:
2026-08-05 19:46:34 +02:00
parent bc0c4c750c
commit d17bb63e23
9 changed files with 120 additions and 85 deletions

View File

@@ -1,4 +1,4 @@
feat: combine mobile navigation and account menu feat: move mobile slot tools into menu
Replace the mobile profile card with a compact page header, account menu, and Brand the mobile header as Foodsharing Manager and move refresh and notification
contextual dashboard toolbar while preserving desktop behavior. actions from the dashboard into the contextual navigation menu.

View File

@@ -25,7 +25,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>Foodsharing Abholung-Konfiguration</title> <title>Foodsharing Manager</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -873,6 +873,10 @@ function App() {
onProtectedNavigate={requestNavigation} onProtectedNavigate={requestNavigation}
profileName={session?.profile?.name} profileName={session?.profile?.name}
onLogout={handleLogout} onLogout={handleLogout}
onRefresh={() => handleRefreshStoresAndPickups({ block: false })}
onToggleNotifications={() => setNotificationPanelOpen((prev) => !prev)}
notificationPanelOpen={notificationPanelOpen}
notificationLoading={notificationLoading}
/> />
<Routes> <Routes>
<Route path="/" element={dashboardContent} /> <Route path="/" element={dashboardContent} />

View File

@@ -760,7 +760,7 @@ const DashboardView = ({
return ( return (
<div className="p-4 max-w-6xl mx-auto bg-white shadow-lg rounded-lg mt-4"> <div className="p-4 max-w-6xl mx-auto bg-white shadow-lg rounded-lg mt-4">
<h1 className="dashboard-page-title text-2xl font-bold mb-4 text-center text-gray-800">Foodsharing Pickup Manager</h1> <h1 className="dashboard-page-title text-2xl font-bold mb-4 text-center text-gray-800">Foodsharing Manager</h1>
<div className="dashboard-summary-card dashboard-session bg-blue-50 border border-blue-100 rounded-lg p-4 mb-4 shadow"> <div className="dashboard-summary-card dashboard-session bg-blue-50 border border-blue-100 rounded-lg p-4 mb-4 shadow">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="dashboard-session__identity"> <div className="dashboard-session__identity">
@@ -803,25 +803,6 @@ const DashboardView = ({
</div> </div>
</div> </div>
<div className="dashboard-mobile-toolbar" aria-label="Slot-Werkzeuge">
<button type="button" className="dashboard-mobile-toolbar__refresh" onClick={() => onRefresh({ block: false })}>
Betriebe aktualisieren
</button>
<button
type="button"
className={`dashboard-mobile-toolbar__notifications ${notificationPanelOpen ? 'is-active' : ''}`}
onClick={onToggleNotificationPanel}
title="Benachrichtigungen konfigurieren"
aria-label="Benachrichtigungen konfigurieren"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.89 3.31.877 2.42 2.42a1.724 1.724 0 0 0 1.065 2.572c1.757.426 1.757 2.924 0 3.35a1.724 1.724 0 0 0-1.066 2.573c.89 1.543-.877 3.31-2.42 2.42a1.724 1.724 0 0 0-2.572 1.065c-.426 1.757-2.924 1.757-3.35 0a1.724 1.724 0 0 0-2.573-1.066c-1.543.89-3.31-.877-2.42-2.42a1.724 1.724 0 0 0-1.065-2.572c-1.757-.426-1.757-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.89-1.543.877-3.31 2.42-2.42.996.575 2.273.155 2.573-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
</svg>
</button>
{notificationLoading && <span className="dashboard-mobile-toolbar__loading">Lade...</span>}
</div>
{notificationPanelOpen && ( {notificationPanelOpen && (
<NotificationPanel <NotificationPanel
error={notificationError} error={notificationError}
@@ -967,7 +948,7 @@ const DashboardView = ({
<span className="text-xs text-gray-500">{plannedPickupEntries.length}</span> <span className="text-xs text-gray-500">{plannedPickupEntries.length}</span>
</div> </div>
{plannedPickupEntries.length === 0 ? ( {plannedPickupEntries.length === 0 ? (
<p className="text-sm text-gray-500">Keine aktiven Planungen im Pickup Manager.</p> <p className="text-sm text-gray-500">Keine aktiven Planungen im Foodsharing Manager.</p>
) : ( ) : (
<ul className="space-y-1.5 max-h-40 overflow-y-auto"> <ul className="space-y-1.5 max-h-40 overflow-y-auto">
{plannedPickupEntries.slice(0, 8).map((entry) => ( {plannedPickupEntries.slice(0, 8).map((entry) => (

View File

@@ -2,10 +2,6 @@
display: none; display: none;
} }
.dashboard-mobile-toolbar {
display: none;
}
@media (max-width: 767px) { @media (max-width: 767px) {
.dashboard-session { .dashboard-session {
display: none; display: none;
@@ -15,54 +11,6 @@
display: none; display: none;
} }
.dashboard-mobile-toolbar {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 0.45rem;
align-items: center;
margin: 0.75rem 0 1rem;
}
.dashboard-mobile-toolbar__refresh,
.dashboard-mobile-toolbar__notifications {
box-sizing: border-box;
min-height: 2.35rem;
border-radius: 5px;
font-size: 0.8125rem;
}
.dashboard-mobile-toolbar__refresh {
width: 100%;
border: 1px solid #2563eb;
background: #2563eb;
color: #fff;
}
.dashboard-mobile-toolbar__notifications {
width: 2.35rem;
height: 2.35rem;
padding: 0;
border: 1px solid #cbd5e1;
background: #fff;
color: #475569;
}
.dashboard-mobile-toolbar__notifications svg {
width: 1rem;
height: 1rem;
}
.dashboard-mobile-toolbar__notifications.is-active {
border-color: #2563eb;
background: #eff6ff;
color: #1d4ed8;
}
.dashboard-mobile-toolbar__loading {
color: #64748b;
font-size: 0.75rem;
}
.mobile-config-list { .mobile-config-list {
display: block; display: block;
padding-bottom: 5.5rem; padding-bottom: 5.5rem;

View File

@@ -50,10 +50,10 @@
border-bottom: 1px solid #cbd5e1; border-bottom: 1px solid #cbd5e1;
} }
.app-navigation__current-page { .app-navigation__product-name {
overflow: hidden; overflow: hidden;
color: #1f2937; color: #1f2937;
font-size: 1rem; font-size: 0.9375rem;
font-weight: 650; font-weight: 650;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
@@ -117,6 +117,52 @@
border-top: 1px solid #dbe4ee; border-top: 1px solid #dbe4ee;
} }
.app-navigation__tools {
display: grid;
gap: 0.35rem;
padding-top: 0.75rem;
border-top: 1px solid #dbe4ee;
}
.app-navigation__tools > span {
color: #64748b;
font-size: 0.75rem;
font-weight: 500;
}
.app-navigation__tool {
display: flex;
align-items: center;
gap: 0.6rem;
width: 100%;
min-height: 2.5rem;
border: 1px solid #dbe4ee;
border-radius: 5px;
padding: 0.55rem 0.7rem;
background: #fff;
color: #334155;
font-size: 0.875rem;
font-weight: 500;
text-align: left;
}
.app-navigation__tool svg {
flex: 0 0 auto;
width: 1rem;
height: 1rem;
}
.app-navigation__tool.is-active {
border-color: #2563eb;
background: #eff6ff;
color: #1d4ed8;
}
.app-navigation__tools small {
color: #64748b;
font-size: 0.75rem;
}
.app-navigation__account div { .app-navigation__account div {
display: grid; display: grid;
min-width: 0; min-width: 0;

View File

@@ -2,7 +2,16 @@ import { useEffect, useMemo, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom'; import { Link, useLocation, useNavigate } from 'react-router-dom';
import './NavigationTabs.css'; import './NavigationTabs.css';
const NavigationTabs = ({ isAdmin, onProtectedNavigate, profileName, onLogout }) => { const NavigationTabs = ({
isAdmin,
onProtectedNavigate,
profileName,
onLogout,
onRefresh,
onToggleNotifications,
notificationPanelOpen,
notificationLoading
}) => {
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
@@ -20,8 +29,6 @@ const NavigationTabs = ({ isAdmin, onProtectedNavigate, profileName, onLogout })
return items; return items;
}, [isAdmin]); }, [isAdmin]);
const activeTab = tabs.find((tab) => tab.to === location.pathname) || tabs[0];
const handleClick = (event, to) => { const handleClick = (event, to) => {
event.preventDefault(); event.preventDefault();
if (to === location.pathname) { if (to === location.pathname) {
@@ -40,6 +47,11 @@ const NavigationTabs = ({ isAdmin, onProtectedNavigate, profileName, onLogout })
setMobileOpen(false); setMobileOpen(false);
}, [location.pathname]); }, [location.pathname]);
const handleToolAction = (action) => {
setMobileOpen(false);
action?.();
};
const renderLink = (tab, className) => { const renderLink = (tab, className) => {
const isActive = location.pathname === tab.to; const isActive = location.pathname === tab.to;
return ( return (
@@ -58,7 +70,7 @@ const NavigationTabs = ({ isAdmin, onProtectedNavigate, profileName, onLogout })
return ( return (
<nav className="app-navigation" aria-label="Navigation"> <nav className="app-navigation" aria-label="Navigation">
<div className="app-navigation__mobile-header"> <div className="app-navigation__mobile-header">
<span className="app-navigation__current-page">{activeTab.label}</span> <span className="app-navigation__product-name">Foodsharing Manager</span>
<button <button
type="button" type="button"
className="app-navigation__menu-toggle" className="app-navigation__menu-toggle"
@@ -89,6 +101,34 @@ const NavigationTabs = ({ isAdmin, onProtectedNavigate, profileName, onLogout })
<div className="app-navigation__mobile-links"> <div className="app-navigation__mobile-links">
{tabs.map((tab) => renderLink(tab, 'app-navigation__mobile-link'))} {tabs.map((tab) => renderLink(tab, 'app-navigation__mobile-link'))}
</div> </div>
{location.pathname === '/' && (onRefresh || onToggleNotifications) && (
<div className="app-navigation__tools">
<span>Werkzeuge</span>
{onRefresh && (
<button type="button" className="app-navigation__tool" onClick={() => handleToolAction(onRefresh)}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M20 11a8.1 8.1 0 0 0-15.5-2M4 5v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2M20 19v-4h-4" />
</svg>
Betriebe aktualisieren
</button>
)}
{onToggleNotifications && (
<button
type="button"
className={`app-navigation__tool ${notificationPanelOpen ? 'is-active' : ''}`}
onClick={() => handleToolAction(onToggleNotifications)}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.89 3.31.877 2.42 2.42a1.724 1.724 0 0 0 1.065 2.572c1.757.426 1.757 2.924 0 3.35a1.724 1.724 0 0 0-1.066 2.573c.89 1.543-.877 3.31-2.42 2.42a1.724 1.724 0 0 0-2.572 1.065c-.426 1.757-2.924 1.757-3.35 0a1.724 1.724 0 0 0-2.573-1.066c-1.543.89-3.31-.877-2.42-2.42a1.724 1.724 0 0 0-1.065-2.572c-1.757-.426-1.757-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.89-1.543.877-3.31 2.42-2.42.996.575 2.273.155 2.573-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0z" />
</svg>
Benachrichtigungen
</button>
)}
{notificationLoading && <small>Lade...</small>}
</div>
)}
<div className="app-navigation__account"> <div className="app-navigation__account">
<div> <div>
<span>Angemeldet</span> <span>Angemeldet</span>

View File

@@ -25,4 +25,20 @@ describe('NavigationTabs', () => {
expect.any(Function) expect.any(Function)
); );
}); });
it('runs slot tools from the menu and closes it afterwards', () => {
const onRefresh = jest.fn();
const onToggleNotifications = jest.fn();
render(<NavigationTabs onRefresh={onRefresh} onToggleNotifications={onToggleNotifications} />);
fireEvent.click(screen.getByRole('button', { name: 'Navigation öffnen' }));
fireEvent.click(screen.getByRole('button', { name: 'Betriebe aktualisieren' }));
expect(onRefresh).toHaveBeenCalledTimes(1);
expect(screen.queryByText('Werkzeuge')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Navigation öffnen' }));
fireEvent.click(screen.getByRole('button', { name: 'Benachrichtigungen' }));
expect(onToggleNotifications).toHaveBeenCalledTimes(1);
});
}); });

View File

@@ -102,7 +102,7 @@ const RangePickerModal = ({
const description = pickup.description ? `, ${pickup.description}` : ''; const description = pickup.description ? `, ${pickup.description}` : '';
return `Foodsharing: ${label}${time ? ` ${time}` : ''}${description}`; return `Foodsharing: ${label}${time ? ` ${time}` : ''}${description}`;
}), }),
...managerPlans.map((plan) => `Pickup Manager: ${plan.label || `Store ${plan.id}`} (${formatRangeText(plan)})`) ...managerPlans.map((plan) => `Foodsharing Manager: ${plan.label || `Store ${plan.id}`} (${formatRangeText(plan)})`)
]; ];
return ( return (
<span <span
@@ -159,7 +159,7 @@ const RangePickerModal = ({
</span> </span>
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
<span className="h-2 w-2 rounded-full bg-emerald-600" /> <span className="h-2 w-2 rounded-full bg-emerald-600" />
Pickup Manager geplant Foodsharing Manager geplant
</span> </span>
</div> </div>
</div> </div>