Delete the src/app tree, next.config.ts, next-env.d.ts, and the Next-only guard/header components. Port AppShell/Sidebar/Topbar to react-router and mount them as a DashboardLayout route over all authenticated routes. Strip 'use client' directives, move globals.css up from the deleted app/ tree, rewrite tsconfig for Vite/Bundler resolution, drop the next dependency. Test fixes the port surfaced: jsdom v29 has no window.matchMedia (sidebar breakpoint) so setup.ts stubs it; the router-boundary specs render inside ThemeProvider because the chrome's ThemeToggle requires the context.
66 lines
1.6 KiB
TypeScript
66 lines
1.6 KiB
TypeScript
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
|
|
|
|
interface SidebarContextValue {
|
|
collapsed: boolean;
|
|
toggleCollapsed: () => void;
|
|
mobileOpen: boolean;
|
|
setMobileOpen: (open: boolean) => void;
|
|
isMobile: boolean;
|
|
}
|
|
|
|
const MOBILE_MAX_WIDTH = 767;
|
|
const SidebarContext = createContext<SidebarContextValue | undefined>(undefined);
|
|
|
|
export function SidebarProvider({ children }: { children: ReactNode }): React.JSX.Element {
|
|
const [collapsed, setCollapsed] = useState(false);
|
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
const [isMobile, setIsMobile] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const mediaQuery = window.matchMedia(`(max-width: ${String(MOBILE_MAX_WIDTH)}px)`);
|
|
|
|
const syncState = (matches: boolean): void => {
|
|
setIsMobile(matches);
|
|
if (!matches) {
|
|
setMobileOpen(false);
|
|
}
|
|
};
|
|
|
|
syncState(mediaQuery.matches);
|
|
|
|
const handleChange = (event: MediaQueryListEvent): void => {
|
|
syncState(event.matches);
|
|
};
|
|
|
|
mediaQuery.addEventListener('change', handleChange);
|
|
|
|
return () => {
|
|
mediaQuery.removeEventListener('change', handleChange);
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<SidebarContext.Provider
|
|
value={{
|
|
collapsed,
|
|
toggleCollapsed: () => setCollapsed((value) => !value),
|
|
mobileOpen,
|
|
setMobileOpen,
|
|
isMobile,
|
|
}}
|
|
>
|
|
{children}
|
|
</SidebarContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useSidebar(): SidebarContextValue {
|
|
const context = useContext(SidebarContext);
|
|
|
|
if (!context) {
|
|
throw new Error('useSidebar must be used within SidebarProvider');
|
|
}
|
|
|
|
return context;
|
|
}
|