feat(v2): add mobile layout
This commit is contained in:
parent
0711c3c003
commit
af1d99d2ff
@ -23,6 +23,7 @@ import { isDev } from './utils/env';
|
|||||||
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
||||||
import { routeTree } from './routeTree.gen';
|
import { routeTree } from './routeTree.gen';
|
||||||
import { DefaultNotFound } from './components/DefaultNotFound';
|
import { DefaultNotFound } from './components/DefaultNotFound';
|
||||||
|
import { TooltipProvider } from './components/ui/tooltip';
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
routeTree,
|
routeTree,
|
||||||
@ -95,7 +96,9 @@ export const App: React.FC = React.memo(() => {
|
|||||||
{isDev ? (
|
{isDev ? (
|
||||||
// Compatible with old routes
|
// Compatible with old routes
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
<RouterProvider router={router} context={{ userInfo }} />
|
<RouterProvider router={router} context={{ userInfo }} />
|
||||||
|
</TooltipProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
) : (
|
) : (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
@ -22,6 +22,7 @@ import { Outlet } from '@tanstack/react-router';
|
|||||||
import { trpc } from '@/api/trpc';
|
import { trpc } from '@/api/trpc';
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
import { LayoutProps } from './types';
|
import { LayoutProps } from './types';
|
||||||
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
|
|
||||||
const defaultLayout: [number, number, number] = [265, 440, 655];
|
const defaultLayout: [number, number, number] = [265, 440, 655];
|
||||||
|
|
||||||
@ -45,6 +46,7 @@ export const DesktopLayout: React.FC<LayoutProps> = React.memo((props) => {
|
|||||||
enabled: !!workspaceId,
|
enabled: !!workspaceId,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const navbar = (
|
const navbar = (
|
||||||
<>
|
<>
|
||||||
@ -61,31 +63,31 @@ export const DesktopLayout: React.FC<LayoutProps> = React.memo((props) => {
|
|||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
links={[
|
links={[
|
||||||
{
|
{
|
||||||
title: 'Website',
|
title: t('Website'),
|
||||||
label: String(serviceCount?.website ?? ''),
|
label: String(serviceCount?.website ?? ''),
|
||||||
icon: LuAreaChart,
|
icon: LuAreaChart,
|
||||||
to: '/website',
|
to: '/website',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Monitor',
|
title: t('Monitor'),
|
||||||
label: String(serviceCount?.monitor ?? ''),
|
label: String(serviceCount?.monitor ?? ''),
|
||||||
icon: LuMonitorDot,
|
icon: LuMonitorDot,
|
||||||
to: '/monitor',
|
to: '/monitor',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Servers',
|
title: t('Servers'),
|
||||||
label: '',
|
label: '',
|
||||||
icon: LuServer,
|
icon: LuServer,
|
||||||
to: '/server',
|
to: '/server',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Telemetry',
|
title: t('Telemetry'),
|
||||||
label: String(serviceCount?.telemetry ?? ''),
|
label: String(serviceCount?.telemetry ?? ''),
|
||||||
icon: LuWifi,
|
icon: LuWifi,
|
||||||
to: '/telemetry',
|
to: '/telemetry',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Pages',
|
title: t('Pages'),
|
||||||
label: String(serviceCount?.page ?? ''),
|
label: String(serviceCount?.page ?? ''),
|
||||||
icon: LuFilePieChart,
|
icon: LuFilePieChart,
|
||||||
to: '/page',
|
to: '/page',
|
||||||
@ -103,7 +105,6 @@ export const DesktopLayout: React.FC<LayoutProps> = React.memo((props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
onLayout={(sizes: number[]) => {
|
onLayout={(sizes: number[]) => {
|
||||||
@ -131,8 +132,7 @@ export const DesktopLayout: React.FC<LayoutProps> = React.memo((props) => {
|
|||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-col',
|
'flex flex-col',
|
||||||
isCollapsed &&
|
isCollapsed && 'min-w-[50px] transition-all duration-300 ease-in-out'
|
||||||
'min-w-[50px] transition-all duration-300 ease-in-out'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{navbar}
|
{navbar}
|
||||||
@ -156,7 +156,6 @@ export const DesktopLayout: React.FC<LayoutProps> = React.memo((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</TooltipProvider>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
DesktopLayout.displayName = 'DesktopLayout';
|
DesktopLayout.displayName = 'DesktopLayout';
|
||||||
|
@ -1,6 +1,105 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
LuAreaChart,
|
||||||
|
LuFilePieChart,
|
||||||
|
LuMenu,
|
||||||
|
LuMonitorDot,
|
||||||
|
LuServer,
|
||||||
|
LuWifi,
|
||||||
|
} from 'react-icons/lu';
|
||||||
|
import { useTranslation } from '@i18next-toolkit/react';
|
||||||
|
import { IconType } from 'react-icons';
|
||||||
|
import { useRouterState, Link, Outlet } from '@tanstack/react-router';
|
||||||
|
import { cn } from '@/utils/style';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { LayoutProps } from './types';
|
||||||
|
import { UserConfig } from './UserConfig';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
export const MobileLayout: React.FC = React.memo(() => {
|
export const MobileLayout: React.FC<LayoutProps> = React.memo((props) => {
|
||||||
return <div>Mobile</div>;
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-svh flex-col">
|
||||||
|
<div className="flex h-[52px] items-center justify-between px-2">
|
||||||
|
<Sheet>
|
||||||
|
<SheetTrigger>
|
||||||
|
<Button variant="outline" size="icon">
|
||||||
|
<LuMenu />
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side="left" className="w-11/12">
|
||||||
|
<ScrollArea className="h-full">{props.list}</ScrollArea>
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<img className="m-auto h-8 w-8" src="/icon.svg" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<UserConfig isCollapsed={true} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{props.children ?? <Outlet />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div className="p-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<MobileNavItem
|
||||||
|
title={t('Website')}
|
||||||
|
icon={LuAreaChart}
|
||||||
|
to="/website"
|
||||||
|
/>
|
||||||
|
<MobileNavItem
|
||||||
|
title={t('Monitor')}
|
||||||
|
icon={LuMonitorDot}
|
||||||
|
to="/monitor"
|
||||||
|
/>
|
||||||
|
<MobileNavItem title={t('Servers')} icon={LuServer} to="/server" />
|
||||||
|
<MobileNavItem title={t('Telemetry')} icon={LuWifi} to="/telemetry" />
|
||||||
|
<MobileNavItem title={t('Pages')} icon={LuFilePieChart} to="/page" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
MobileLayout.displayName = 'MobileLayout';
|
MobileLayout.displayName = 'MobileLayout';
|
||||||
|
|
||||||
|
const MobileNavItem: React.FC<{
|
||||||
|
title: string;
|
||||||
|
icon: IconType;
|
||||||
|
to: string;
|
||||||
|
}> = React.memo((props) => {
|
||||||
|
const pathname = useRouterState({
|
||||||
|
select: (state) => state.location.pathname,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSelect = pathname.startsWith(props.to);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className={cn(
|
||||||
|
'flex-1 rounded-lg p-1 text-center',
|
||||||
|
isSelect
|
||||||
|
? 'bg-muted text-black dark:text-white'
|
||||||
|
: 'text-muted-foreground'
|
||||||
|
)}
|
||||||
|
to={props.to}
|
||||||
|
>
|
||||||
|
<props.icon size={24} className="m-auto mb-1" />
|
||||||
|
<div className={cn('text-sm font-semibold', isSelect && 'font-bold')}>
|
||||||
|
{props.title}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
MobileNavItem.displayName = 'MobileNavItem';
|
||||||
|
@ -16,7 +16,7 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { useEvent } from '@/hooks/useEvent';
|
import { useEvent } from '@/hooks/useEvent';
|
||||||
import { useSettingsStore } from '@/store/settings';
|
import { useSettingsStore } from '@/store/settings';
|
||||||
import { useUserInfo } from '@/store/user';
|
import { useCurrentWorkspaceId, useUserInfo, useUserStore } from '@/store/user';
|
||||||
import { languages } from '@/utils/constants';
|
import { languages } from '@/utils/constants';
|
||||||
import { useTranslation, setLanguage } from '@i18next-toolkit/react';
|
import { useTranslation, setLanguage } from '@i18next-toolkit/react';
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
@ -32,6 +32,20 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
|||||||
const { i18n, t } = useTranslation();
|
const { i18n, t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const colorScheme = useSettingsStore((state) => state.colorScheme);
|
const colorScheme = useSettingsStore((state) => state.colorScheme);
|
||||||
|
const workspaceId = useCurrentWorkspaceId();
|
||||||
|
const workspaces = useUserStore((state) => {
|
||||||
|
const userInfo = state.info;
|
||||||
|
if (userInfo) {
|
||||||
|
return userInfo.workspaces.map((w) => ({
|
||||||
|
id: w.workspace.id,
|
||||||
|
name: w.workspace.name,
|
||||||
|
role: w.role,
|
||||||
|
current: userInfo.currentWorkspace?.id === w.workspace.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
const handleChangeColorSchema = useEvent((colorScheme) => {
|
const handleChangeColorSchema = useEvent((colorScheme) => {
|
||||||
useSettingsStore.setState({
|
useSettingsStore.setState({
|
||||||
@ -97,6 +111,25 @@ export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
|
|||||||
{t('Notifications')}
|
{t('Notifications')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>{t('Workspaces')}</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuSubContent>
|
||||||
|
<DropdownMenuRadioGroup value={workspaceId}>
|
||||||
|
{workspaces.map((workspace) => (
|
||||||
|
<DropdownMenuRadioItem
|
||||||
|
key={workspace.id}
|
||||||
|
value={workspace.id}
|
||||||
|
disabled={true}
|
||||||
|
>
|
||||||
|
{workspace.name}
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
|
||||||
<DropdownMenuSub>
|
<DropdownMenuSub>
|
||||||
<DropdownMenuSubTrigger>{t('Language')}</DropdownMenuSubTrigger>
|
<DropdownMenuSubTrigger>{t('Language')}</DropdownMenuSubTrigger>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
|
@ -8,7 +8,7 @@ export const LayoutV2: React.FC<LayoutProps> = React.memo((props) => {
|
|||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return <MobileLayout />;
|
return <MobileLayout {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <DesktopLayout {...props} />;
|
return <DesktopLayout {...props} />;
|
||||||
|
Loading…
Reference in New Issue
Block a user