tianji/src/client/components/layout/UserConfig.tsx
2024-11-11 01:33:06 +08:00

246 lines
7.4 KiB
TypeScript

import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuLabel,
} from '@/components/ui/dropdown-menu';
import { useEvent } from '@/hooks/useEvent';
import { useSettingsStore } from '@/store/settings';
import {
setUserInfo,
useCurrentWorkspace,
useCurrentWorkspaceId,
useUserInfo,
useUserStore,
} from '@/store/user';
import { languages } from '@/utils/i18n';
import { useTranslation, setLanguage } from '@i18next-toolkit/react';
import { useNavigate } from '@tanstack/react-router';
import { version } from '@/utils/env';
import React from 'react';
import { LuMoreVertical } from 'react-icons/lu';
import { trpc } from '@/api/trpc';
import { useSocketConnected } from '@/api/socketio';
import { cn } from '@/utils/style';
interface UserConfigProps {
isCollapsed: boolean;
}
export const UserConfig: React.FC<UserConfigProps> = React.memo((props) => {
const userInfo = useUserInfo();
const { i18n, t } = useTranslation();
const navigate = useNavigate();
const colorScheme = useSettingsStore((state) => state.colorScheme);
const workspaceId = useCurrentWorkspaceId();
const currentWorkspace = useCurrentWorkspace();
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: currentWorkspace.id === w.workspace.id,
}));
}
return [];
});
const switchWorkspaceMutation = trpc.workspace.switch.useMutation({
onSuccess: (userInfo) => {
setUserInfo(userInfo);
},
});
const socketConnected = useSocketConnected();
const handleChangeColorSchema = useEvent((colorScheme) => {
useSettingsStore.setState({
colorScheme,
});
});
const nickname = userInfo?.nickname ?? userInfo?.username ?? '';
const avatar = (
<div className="relative">
<Avatar size={props.isCollapsed ? 'sm' : 'default'}>
{userInfo?.avatar && <AvatarImage src={userInfo.avatar} />}
<AvatarFallback delayMs={userInfo?.avatar ? undefined : 0}>
{nickname.substring(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div
className={cn(
'absolute bottom-0 right-0 h-2 w-2 rounded-full border border-white border-opacity-50',
socketConnected ? 'bg-green-400' : 'bg-gray-400'
)}
/>
</div>
);
const name = (
<div className="flex-1 overflow-hidden text-ellipsis" title={nickname}>
{nickname}
</div>
);
const more = (
<Button variant="outline" size="icon" className="shrink-0">
<LuMoreVertical />
</Button>
);
return (
<div className="flex items-center gap-2 p-2">
<DropdownMenu>
{props.isCollapsed ? (
<>
<DropdownMenuTrigger asChild={true} className="cursor-pointer">
{avatar}
</DropdownMenuTrigger>
</>
) : (
<>
{avatar}
{name}
<DropdownMenuTrigger asChild={true} className="cursor-pointer">
{more}
</DropdownMenuTrigger>
</>
)}
<DropdownMenuContent>
<DropdownMenuItem
onClick={() =>
navigate({
to: '/settings/profile',
})
}
>
{t('Profile')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() =>
navigate({
to: '/settings/notifications',
})
}
>
{t('Notifications')}
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>{t('Workspaces')}</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup value={workspaceId}>
{workspaces.map((workspace) => (
<DropdownMenuRadioItem
key={workspace.id}
value={workspace.id}
disabled={workspace.id === workspaceId}
onSelect={() =>
switchWorkspaceMutation.mutateAsync({
workspaceId: workspace.id,
})
}
>
{workspace.name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>{t('Language')}</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={i18n.language ?? 'en'}
onValueChange={setLanguage}
>
{languages.map((language) => (
<DropdownMenuRadioItem
key={language.key}
value={language.key}
>
{language.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>{t('Theme')}</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={colorScheme}
onValueChange={handleChangeColorSchema}
>
<DropdownMenuRadioItem value={'dark'}>
{t('Dark')}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value={'light'}>
{t('Light')}
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => window.open('https://tianji.msgbyte.com/docs/intro')}
>
{t('Document')}
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>{t('Community')}</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem
onClick={() => window.open('https://discord.gg/8Vv47wAEej')}
>
{t('Join Discord')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => window.open('https://twitter.com/moonrailgun')}
>
{t('Follow Twitter')}
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuLabel className="text-gray-500">
v{version}
</DropdownMenuLabel>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
});
UserConfig.displayName = 'UserConfig';