Files
AFFiNE-Mirror/packages/frontend/admin/src/modules/layout.tsx
2024-08-14 03:34:38 +00:00

261 lines
6.5 KiB
TypeScript

import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@affine/admin/components/ui/resizable';
import { Separator } from '@affine/admin/components/ui/separator';
import { TooltipProvider } from '@affine/admin/components/ui/tooltip';
import { cn } from '@affine/admin/utils';
import { AlignJustifyIcon } from 'lucide-react';
import type { PropsWithChildren, ReactNode, RefObject } from 'react';
import {
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import type { ImperativePanelHandle } from 'react-resizable-panels';
import { Button } from '../components/ui/button';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '../components/ui/sheet';
import { Logo } from './accounts/components/logo';
import { NavContext } from './nav/context';
import { Nav } from './nav/nav';
interface RightPanelContextType {
isOpen: boolean;
rightPanelContent: ReactNode;
setRightPanelContent: (content: ReactNode) => void;
togglePanel: () => void;
openPanel: () => void;
closePanel: () => void;
}
const RightPanelContext = createContext<RightPanelContextType | undefined>(
undefined
);
export const useRightPanel = () => {
const context = useContext(RightPanelContext);
if (!context) {
throw new Error('useRightPanel must be used within a RightPanelProvider');
}
return context;
};
export function useMediaQuery(query: string) {
const [value, setValue] = useState(false);
useEffect(() => {
function onChange(event: MediaQueryListEvent) {
setValue(event.matches);
}
const result = matchMedia(query);
result.addEventListener('change', onChange);
setValue(result.matches);
return () => result.removeEventListener('change', onChange);
}, [query]);
return value;
}
export function Layout({ children }: PropsWithChildren) {
const [rightPanelContent, setRightPanelContent] = useState<ReactNode>(null);
const [open, setOpen] = useState(false);
const rightPanelRef = useRef<ImperativePanelHandle>(null);
const [activeTab, setActiveTab] = useState('');
const [activeSubTab, setActiveSubTab] = useState('auth');
const [currentModule, setCurrentModule] = useState('auth');
const handleExpand = useCallback(() => {
if (rightPanelRef.current?.getSize() === 0) {
rightPanelRef.current?.resize(30);
}
setOpen(true);
}, [rightPanelRef]);
const handleCollapse = useCallback(() => {
if (rightPanelRef.current?.getSize() !== 0) {
rightPanelRef.current?.resize(0);
}
setOpen(false);
}, [rightPanelRef]);
const openPanel = useCallback(() => {
handleExpand();
rightPanelRef.current?.expand();
}, [handleExpand]);
const closePanel = useCallback(() => {
handleCollapse();
rightPanelRef.current?.collapse();
}, [handleCollapse]);
const togglePanel = useCallback(
() => (rightPanelRef.current?.isCollapsed() ? openPanel() : closePanel()),
[closePanel, openPanel]
);
return (
<RightPanelContext.Provider
value={{
isOpen: open,
rightPanelContent,
setRightPanelContent,
togglePanel,
openPanel,
closePanel,
}}
>
<NavContext.Provider
value={{
activeTab,
activeSubTab,
currentModule,
setActiveTab,
setActiveSubTab,
setCurrentModule,
}}
>
<TooltipProvider delayDuration={0}>
<div className="flex">
<LeftPanel />
<ResizablePanelGroup direction="horizontal">
<ResizablePanel id="0" order={0} minSize={50}>
{children}
</ResizablePanel>
<RightPanel
rightPanelRef={rightPanelRef}
onExpand={handleExpand}
onCollapse={handleCollapse}
/>
</ResizablePanelGroup>
</div>
</TooltipProvider>
</NavContext.Provider>
</RightPanelContext.Provider>
);
}
export const LeftPanel = () => {
const isSmallScreen = useMediaQuery('(max-width: 768px)');
if (isSmallScreen) {
return (
<Sheet>
<SheetTrigger asChild>
<Button variant="ghost" className="fixed top-4 left-6 p-0 h-5 w-5">
<AlignJustifyIcon size={20} />
</Button>
</SheetTrigger>
<SheetHeader className="hidden">
<SheetTitle>AFFiNE</SheetTitle>
<SheetDescription>
Admin panel for managing accounts, AI, config, and settings
</SheetDescription>
</SheetHeader>
<SheetContent side="left" className="p-0" withoutCloseButton>
<div className="flex flex-col w-full h-full">
<div
className={cn(
'flex h-[52px] items-center gap-2 px-4 text-base font-medium'
)}
>
<Logo />
AFFiNE
</div>
<Separator />
<Nav />
</div>
</SheetContent>
</Sheet>
);
}
return (
<div className="flex flex-col min-w-52 max-w-sm border-r">
<div
className={cn(
'flex h-[52px] items-center gap-2 px-4 text-base font-medium'
)}
>
<Logo />
AFFiNE
</div>
<Separator />
<Nav />
</div>
);
};
export const RightPanel = ({
rightPanelRef,
onExpand,
onCollapse,
}: {
rightPanelRef: RefObject<ImperativePanelHandle>;
onExpand: () => void;
onCollapse: () => void;
}) => {
const isSmallScreen = useMediaQuery('(max-width: 768px)');
const { rightPanelContent, isOpen } = useRightPanel();
const onOpenChange = useCallback(
(open: boolean) => {
if (open) {
onExpand();
} else {
onCollapse();
}
},
[onExpand, onCollapse]
);
if (isSmallScreen) {
return (
<Sheet open={isOpen} onOpenChange={onOpenChange}>
<SheetHeader className="hidden">
<SheetTitle>Right Panel</SheetTitle>
<SheetDescription>
For displaying additional information
</SheetDescription>
</SheetHeader>
<SheetContent side="right" className="p-0" withoutCloseButton>
{rightPanelContent}
</SheetContent>
</Sheet>
);
}
return (
<>
<ResizableHandle />
<ResizablePanel
id="1"
order={1}
ref={rightPanelRef}
defaultSize={0}
maxSize={30}
collapsible={true}
collapsedSize={0}
onExpand={onExpand}
onCollapse={onCollapse}
>
{rightPanelContent}
</ResizablePanel>
</>
);
};