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:
@@ -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.
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user