feat(10-04): AdminPage + /admin route + conditional nav entries + e2e spec
- AdminPage: Admin Settings heading, MEMBERS section (avatar+status+action), SHARED CALENDAR radio group + two-tap Save + empty state - App.tsx: /admin route gated by meQuery.data.user.isAdmin (loading gate prevents flash), SetupBanner mounted above content, BottomTabBar + AppNav receive isAdmin - AppNav.tsx: ShieldCheck Admin nav entry rendered only when isAdmin=true (D-03 UX gating) - BottomTabBar.tsx: ShieldCheck Admin tab rendered only when isAdmin=true (D-03 UX gating) - e2e/admin.spec.ts: 5 assertions across 3 profiles (15 total tests) — admin sees nav+page+members, non-admin: no nav entry + /admin redirects to /calendar - All 15 e2e tests pass (iphone/pixel/desktop); production build clean
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import { NavLink } from 'react-router';
|
||||
import { CalendarDays, List } from 'lucide-react';
|
||||
import { CalendarDays, List, ShieldCheck } from 'lucide-react';
|
||||
import { ColorLegend, type LegendMember } from './ColorLegend.js';
|
||||
|
||||
interface AppNavProps {
|
||||
@@ -21,6 +21,8 @@ interface AppNavProps {
|
||||
currentUserName?: string;
|
||||
/** Called when the user avatar is tapped — opens the Settings sheet. */
|
||||
onOpenSettings?: () => void;
|
||||
/** When true, renders the Admin nav entry (ShieldCheck). UX gating only (D-03). */
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
export function AppNav({
|
||||
@@ -28,6 +30,7 @@ export function AppNav({
|
||||
currentUserColor,
|
||||
currentUserName,
|
||||
onOpenSettings,
|
||||
isAdmin = false,
|
||||
}: AppNavProps) {
|
||||
const isMobile = typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches;
|
||||
|
||||
@@ -47,6 +50,7 @@ export function AppNav({
|
||||
currentUserColor={currentUserColor}
|
||||
currentUserName={currentUserName}
|
||||
onOpenSettings={onOpenSettings}
|
||||
isAdmin={isAdmin}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -129,11 +133,13 @@ function DesktopNav({
|
||||
currentUserColor,
|
||||
currentUserName,
|
||||
onOpenSettings,
|
||||
isAdmin = false,
|
||||
}: {
|
||||
members: LegendMember[];
|
||||
currentUserColor?: string;
|
||||
currentUserName?: string;
|
||||
onOpenSettings?: () => void;
|
||||
isAdmin?: boolean;
|
||||
}) {
|
||||
const navLinkStyle = ({ isActive }: { isActive: boolean }): React.CSSProperties => ({
|
||||
display: 'flex',
|
||||
@@ -198,6 +204,13 @@ function DesktopNav({
|
||||
<List size={18} aria-hidden="true" />
|
||||
Lists
|
||||
</NavLink>
|
||||
{/* Admin entry — only when isAdmin=true (UX gating, D-03) */}
|
||||
{isAdmin && (
|
||||
<NavLink to="/admin" style={navLinkStyle} aria-label="Admin settings">
|
||||
<ShieldCheck size={18} aria-hidden="true" />
|
||||
Admin
|
||||
</NavLink>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Color legend */}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import { NavLink } from 'react-router';
|
||||
import { CalendarDays, List } from 'lucide-react';
|
||||
import { CalendarDays, List, ShieldCheck } from 'lucide-react';
|
||||
|
||||
function isPhone(): boolean {
|
||||
return typeof window !== 'undefined' && window.matchMedia('(max-width: 767px)').matches;
|
||||
@@ -49,7 +49,12 @@ const tabActiveOverride: React.CSSProperties = {
|
||||
borderBottom: '2px solid var(--color-member-0)',
|
||||
};
|
||||
|
||||
export function BottomTabBar() {
|
||||
interface BottomTabBarProps {
|
||||
/** When true, renders the Admin tab (ShieldCheck). UX gating only (D-03). */
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
export function BottomTabBar({ isAdmin = false }: BottomTabBarProps) {
|
||||
// Phone-only: return null on desktop (≥768px) so the fixed bar does not overlay
|
||||
// the AppNav sidebar's Settings/avatar button (FIX 4). Consistent with the
|
||||
// isPhone() breakpoint used in AppNav and CalendarShell.
|
||||
@@ -96,6 +101,21 @@ export function BottomTabBar() {
|
||||
<List size={22} aria-hidden="true" />
|
||||
<span>Lists</span>
|
||||
</NavLink>
|
||||
|
||||
{/* Admin tab — only when isAdmin=true (UX gating, D-03) */}
|
||||
{isAdmin && (
|
||||
<NavLink
|
||||
to="/admin"
|
||||
aria-label="Admin settings"
|
||||
style={({ isActive }) => ({
|
||||
...tabBase,
|
||||
...(isActive ? tabActiveOverride : {}),
|
||||
})}
|
||||
>
|
||||
<ShieldCheck size={22} aria-hidden="true" />
|
||||
<span>Admin</span>
|
||||
</NavLink>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user