- Updated NativeCancelSubscriptionModal to use translation keys for titles and messages. - Enhanced CircleSetupView with translation for notifications and UI text. - Refactored ThingsView to implement translation for notifications, titles, and descriptions. - Added translation support in AdvancedOptionsSection for labels and descriptions. - Implemented i18n in RepeatPickerField for various UI elements and labels.
551 lines
18 KiB
JavaScript
551 lines
18 KiB
JavaScript
import {
|
|
AdminPanelSettings,
|
|
DarkModeOutlined,
|
|
GroupAdd,
|
|
LightModeOutlined,
|
|
Logout,
|
|
Person,
|
|
Settings,
|
|
SwapHoriz,
|
|
Tune,
|
|
WorkspacePremium,
|
|
} from '@mui/icons-material'
|
|
import {
|
|
Avatar,
|
|
Box,
|
|
Divider,
|
|
Dropdown,
|
|
ListItemContent,
|
|
ListItemDecorator,
|
|
Menu,
|
|
MenuButton,
|
|
MenuItem,
|
|
Sheet,
|
|
Typography,
|
|
useColorScheme,
|
|
} from '@mui/joy'
|
|
import { useMediaQuery } from '@mui/material'
|
|
import moment from 'moment'
|
|
import { useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useNavigate } from 'react-router-dom'
|
|
|
|
import { useImpersonateUser } from '../contexts/ImpersonateUserContext'
|
|
import useStickyState from '../hooks/useStickyState'
|
|
import { useCircleMembers, useUserProfile } from '../queries/UserQueries'
|
|
import { apiClient } from '../utils/ApiClient'
|
|
import { isPlusAccount, resolvePhotoURL } from '../utils/Helpers'
|
|
import UserModal from '../views/Modals/Inputs/UserModal'
|
|
import SubscriptionModal from './SubscriptionModal'
|
|
|
|
const UserProfileAvatar = () => {
|
|
const { t } = useTranslation('common')
|
|
const navigate = useNavigate()
|
|
const { mode, setMode } = useColorScheme()
|
|
const { data: userProfile } = useUserProfile()
|
|
const {
|
|
canImpersonate,
|
|
getEffectiveUser,
|
|
isImpersonating,
|
|
startImpersonation,
|
|
stopImpersonation,
|
|
} = useImpersonateUser()
|
|
const { data: circleMembersData } = useCircleMembers()
|
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
|
const [isSubscriptionModalOpen, setIsSubscriptionModalOpen] = useState(false)
|
|
const [themeMode, setThemeMode] = useStickyState(mode, 'themeMode')
|
|
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('lg'))
|
|
|
|
if (!userProfile) return null
|
|
|
|
const currentUser = getEffectiveUser(userProfile)
|
|
const isAdmin = canImpersonate(userProfile, circleMembersData?.res)
|
|
const isPlusUser = isPlusAccount(userProfile)
|
|
|
|
const getSubscriptionStatus = () => {
|
|
if (!userProfile) return t('userMenu.planFree')
|
|
|
|
if (userProfile.subscription === 'active') {
|
|
return t('userMenu.planPlus')
|
|
}
|
|
|
|
if (
|
|
userProfile.subscription === 'cancelled' &&
|
|
moment().isBefore(userProfile.expiration)
|
|
) {
|
|
return t('userMenu.planPlusExpiring')
|
|
}
|
|
|
|
return t('userMenu.planFree')
|
|
}
|
|
|
|
const handleLogout = () => {
|
|
apiClient.handleLogout()
|
|
}
|
|
|
|
const handleSupportEmail = () => {
|
|
window.location.href = 'mailto:support@donetick.com'
|
|
}
|
|
|
|
const isDarkMode = themeMode === 'dark'
|
|
|
|
const handleThemeToggle = () => {
|
|
const newThemeMode = isDarkMode ? 'light' : 'dark'
|
|
setThemeMode(newThemeMode)
|
|
setMode(newThemeMode)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Dropdown>
|
|
<MenuButton
|
|
variant='plain'
|
|
sx={{
|
|
p: 0,
|
|
border: 'none',
|
|
backgroundColor: 'transparent',
|
|
borderRadius: '50%',
|
|
'&:hover': {
|
|
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
|
// transform: 'scale(1.05)',
|
|
// transition: 'all 0.2s ease',
|
|
},
|
|
'&:active': {
|
|
// transform: 'scale(0.95)',
|
|
},
|
|
}}
|
|
>
|
|
<Box sx={{ position: 'relative' }}>
|
|
{isImpersonating ? (
|
|
<Box sx={{ position: 'relative' }}>
|
|
<Avatar
|
|
src={resolvePhotoURL(currentUser?.image)}
|
|
alt={currentUser?.displayName || currentUser?.name}
|
|
size='md'
|
|
sx={{
|
|
width: 36,
|
|
height: 36,
|
|
border: '2px solid var(--joy-palette-background-surface)',
|
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
|
}}
|
|
/>
|
|
<Avatar
|
|
src={resolvePhotoURL(
|
|
userProfile?.image || userProfile?.avatar,
|
|
)}
|
|
alt={userProfile?.displayName || userProfile?.name}
|
|
size='sm'
|
|
sx={{
|
|
position: 'absolute',
|
|
bottom: -2,
|
|
left: -2,
|
|
width: 18,
|
|
height: 18,
|
|
border: '2px solid var(--joy-palette-background-surface)',
|
|
backgroundColor: 'var(--joy-palette-background-surface)',
|
|
boxShadow: '0 1px 4px rgba(0,0,0,0.2)',
|
|
}}
|
|
/>
|
|
<Box
|
|
sx={{
|
|
position: 'absolute',
|
|
top: -2,
|
|
right: -2,
|
|
width: 14,
|
|
height: 14,
|
|
borderRadius: '50%',
|
|
backgroundColor: 'var(--joy-palette-primary-500)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
border: '1px solid var(--joy-palette-background-surface)',
|
|
boxShadow: '0 1px 2px rgba(0,0,0,0.2)',
|
|
}}
|
|
>
|
|
<SwapHoriz sx={{ fontSize: 8, color: 'white' }} />
|
|
</Box>
|
|
</Box>
|
|
) : (
|
|
<Avatar
|
|
src={resolvePhotoURL(currentUser?.image || currentUser?.avatar)}
|
|
alt={currentUser?.displayName || currentUser?.name}
|
|
size='md'
|
|
sx={{
|
|
width: 36,
|
|
height: 36,
|
|
border: '2px solid var(--joy-palette-background-surface)',
|
|
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
</MenuButton>
|
|
<Menu
|
|
placement='bottom-end'
|
|
sx={{
|
|
minWidth: 280,
|
|
p: 1,
|
|
'--List-gap': '4px',
|
|
boxShadow: 'var(--joy-shadow-lg)',
|
|
border: '1px solid var(--joy-palette-divider)',
|
|
borderRadius: 'var(--joy-radius-md)',
|
|
}}
|
|
>
|
|
<Sheet sx={{ p: 2, borderRadius: 'var(--joy-radius-sm)', mb: 1 }}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
<Avatar
|
|
src={resolvePhotoURL(currentUser?.image || currentUser?.avatar)}
|
|
alt={currentUser?.displayName || currentUser?.name}
|
|
size='lg'
|
|
sx={{
|
|
width: 48,
|
|
height: 48,
|
|
border: '2px solid var(--joy-palette-background-surface)',
|
|
}}
|
|
/>
|
|
<Box sx={{ flex: 1, minWidth: 0 }}>
|
|
<Typography
|
|
level='title-md'
|
|
sx={{
|
|
fontWeight: 600,
|
|
color: 'var(--joy-palette-text-primary)',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
mb: 0.25,
|
|
}}
|
|
>
|
|
{currentUser?.displayName || currentUser?.name}
|
|
</Typography>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
<Typography
|
|
level='body-sm'
|
|
sx={{
|
|
color: 'var(--joy-palette-text-tertiary)',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
flex: 1,
|
|
}}
|
|
>
|
|
{currentUser?.email}
|
|
</Typography>
|
|
{isPlusUser && (
|
|
<Typography
|
|
level='body-xs'
|
|
sx={{
|
|
color: 'var(--joy-palette-warning-600)',
|
|
fontWeight: 500,
|
|
fontSize: '11px',
|
|
}}
|
|
>
|
|
{getSubscriptionStatus()}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
{isImpersonating && (
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 0.5,
|
|
mt: 0.5,
|
|
px: 1,
|
|
py: 0.25,
|
|
backgroundColor: 'var(--joy-palette-primary-softBg)',
|
|
borderRadius: 'var(--joy-radius-sm)',
|
|
width: 'fit-content',
|
|
}}
|
|
>
|
|
<SwapHoriz
|
|
sx={{
|
|
fontSize: 12,
|
|
color: 'var(--joy-palette-primary-600)',
|
|
}}
|
|
/>
|
|
<Typography
|
|
level='body-xs'
|
|
sx={{
|
|
color: 'var(--joy-palette-primary-600)',
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
{t('userMenu.impersonating')}
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
</Box>
|
|
</Sheet>
|
|
|
|
{isAdmin && (
|
|
<>
|
|
<MenuItem
|
|
onClick={() => setIsModalOpen(true)}
|
|
sx={{
|
|
borderRadius: 'var(--joy-radius-sm)',
|
|
'&:hover': {
|
|
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
|
},
|
|
}}
|
|
>
|
|
<ListItemDecorator
|
|
sx={{ color: 'var(--joy-palette-primary-500)' }}
|
|
>
|
|
<AdminPanelSettings />
|
|
</ListItemDecorator>
|
|
<ListItemContent>
|
|
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
|
{isImpersonating
|
|
? t('userMenu.switchUser')
|
|
: t('userMenu.impersonateUser')}
|
|
</Typography>
|
|
<Typography
|
|
level='body-xs'
|
|
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
|
>
|
|
{t('userMenu.actAsAnother')}
|
|
</Typography>
|
|
</ListItemContent>
|
|
</MenuItem>
|
|
|
|
{isImpersonating && (
|
|
<MenuItem
|
|
onClick={() => stopImpersonation()}
|
|
sx={{
|
|
borderRadius: 'var(--joy-radius-sm)',
|
|
'&:hover': {
|
|
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
|
},
|
|
}}
|
|
>
|
|
<ListItemDecorator
|
|
sx={{ color: 'var(--joy-palette-success-500)' }}
|
|
>
|
|
<Person />
|
|
</ListItemDecorator>
|
|
<ListItemContent>
|
|
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
|
{t('userMenu.stopImpersonating')}
|
|
</Typography>
|
|
<Typography
|
|
level='body-xs'
|
|
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
|
>
|
|
{t('userMenu.returnToAccount')}
|
|
</Typography>
|
|
</ListItemContent>
|
|
</MenuItem>
|
|
)}
|
|
|
|
<Divider sx={{ my: 1 }} />
|
|
</>
|
|
)}
|
|
|
|
<MenuItem
|
|
onClick={() => navigate('/settings')}
|
|
sx={{
|
|
borderRadius: 'var(--joy-radius-sm)',
|
|
'&:hover': {
|
|
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
|
},
|
|
}}
|
|
>
|
|
<ListItemDecorator sx={{ color: 'var(--joy-palette-neutral-500)' }}>
|
|
<Settings />
|
|
</ListItemDecorator>
|
|
<ListItemContent>
|
|
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
|
{t('settings')}
|
|
</Typography>
|
|
<Typography
|
|
level='body-xs'
|
|
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
|
>
|
|
{t('userMenu.accountPrefs')}
|
|
</Typography>
|
|
</ListItemContent>
|
|
</MenuItem>
|
|
|
|
<MenuItem
|
|
onClick={() => navigate('/settings/circle')}
|
|
sx={{
|
|
borderRadius: 'var(--joy-radius-sm)',
|
|
'&:hover': {
|
|
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
|
},
|
|
}}
|
|
>
|
|
<ListItemDecorator sx={{ color: 'var(--joy-palette-primary-500)' }}>
|
|
<GroupAdd />
|
|
</ListItemDecorator>
|
|
<ListItemContent>
|
|
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
|
{t('userMenu.invitePeople')}
|
|
</Typography>
|
|
<Typography
|
|
level='body-xs'
|
|
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
|
>
|
|
{t('userMenu.addMembers')}
|
|
</Typography>
|
|
</ListItemContent>
|
|
</MenuItem>
|
|
{isLargeScreen && (
|
|
<MenuItem
|
|
onClick={() => navigate('/settings/detailed#sidepanel')}
|
|
sx={{
|
|
borderRadius: 'var(--joy-radius-sm)',
|
|
'&:hover': {
|
|
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
|
},
|
|
}}
|
|
>
|
|
<ListItemDecorator
|
|
sx={{ color: 'var(--joy-palette-neutral-500)' }}
|
|
>
|
|
<Tune />
|
|
</ListItemDecorator>
|
|
<ListItemContent>
|
|
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
|
{t('userMenu.sidePanelSettings')}
|
|
</Typography>
|
|
<Typography
|
|
level='body-xs'
|
|
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
|
>
|
|
Customize layout & cards
|
|
</Typography>
|
|
</ListItemContent>
|
|
</MenuItem>
|
|
)}
|
|
|
|
<MenuItem
|
|
onClick={handleThemeToggle}
|
|
sx={{
|
|
borderRadius: 'var(--joy-radius-sm)',
|
|
'&:hover': {
|
|
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
|
},
|
|
}}
|
|
>
|
|
<ListItemDecorator sx={{ color: 'var(--joy-palette-neutral-500)' }}>
|
|
{isDarkMode ? <LightModeOutlined /> : <DarkModeOutlined />}
|
|
</ListItemDecorator>
|
|
<ListItemContent>
|
|
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
|
{isDarkMode
|
|
? t('userMenu.switchToLight')
|
|
: t('userMenu.switchToDark')}
|
|
</Typography>
|
|
<Typography
|
|
level='body-xs'
|
|
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
|
>
|
|
{t('userMenu.toggleThemeAppearance')}
|
|
</Typography>
|
|
</ListItemContent>
|
|
</MenuItem>
|
|
|
|
{!isPlusUser && (
|
|
<MenuItem
|
|
onClick={() => navigate('/settings/account')}
|
|
sx={{
|
|
borderRadius: 'var(--joy-radius-sm)',
|
|
'&:hover': {
|
|
backgroundColor: 'var(--joy-palette-warning-softHoverBg)',
|
|
},
|
|
}}
|
|
>
|
|
<ListItemDecorator
|
|
sx={{ color: 'var(--joy-palette-warning-500)' }}
|
|
>
|
|
<WorkspacePremium />
|
|
</ListItemDecorator>
|
|
<ListItemContent>
|
|
<Typography
|
|
level='body-sm'
|
|
sx={{
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
{t('userMenu.upgradeToPlus')}
|
|
</Typography>
|
|
<Typography level='body-xs'>
|
|
{t('userMenu.unlockPremium')}
|
|
</Typography>
|
|
</ListItemContent>
|
|
</MenuItem>
|
|
)}
|
|
|
|
{/* <MenuItem
|
|
onClick={handleSupportEmail}
|
|
sx={{
|
|
borderRadius: 'var(--joy-radius-sm)',
|
|
'&:hover': {
|
|
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
|
},
|
|
}}
|
|
>
|
|
<ListItemDecorator sx={{ color: 'var(--joy-palette-info-500)' }}>
|
|
<Email />
|
|
</ListItemDecorator>
|
|
<ListItemContent>
|
|
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
|
Support
|
|
</Typography>
|
|
<Typography
|
|
level='body-xs'
|
|
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
|
>
|
|
support@donetick.com
|
|
</Typography>
|
|
</ListItemContent>
|
|
</MenuItem> */}
|
|
|
|
<Divider sx={{ my: 1 }} />
|
|
|
|
<MenuItem
|
|
onClick={handleLogout}
|
|
sx={{
|
|
borderRadius: 'var(--joy-radius-sm)',
|
|
'&:hover': {
|
|
backgroundColor: 'var(--joy-palette-danger-softHoverBg)',
|
|
},
|
|
}}
|
|
>
|
|
<ListItemDecorator sx={{ color: 'var(--joy-palette-danger-500)' }}>
|
|
<Logout />
|
|
</ListItemDecorator>
|
|
<ListItemContent>
|
|
<Typography
|
|
level='body-sm'
|
|
sx={{ fontWeight: 500, color: 'var(--joy-palette-danger-500)' }}
|
|
>
|
|
{t('logout')}
|
|
</Typography>
|
|
</ListItemContent>
|
|
</MenuItem>
|
|
</Menu>
|
|
</Dropdown>
|
|
|
|
<UserModal
|
|
isOpen={isModalOpen}
|
|
performers={circleMembersData?.res}
|
|
onSelect={user => {
|
|
startImpersonation(user, userProfile)
|
|
setIsModalOpen(false)
|
|
}}
|
|
onClose={() => setIsModalOpen(false)}
|
|
/>
|
|
|
|
<SubscriptionModal
|
|
open={isSubscriptionModalOpen}
|
|
onClose={() => setIsSubscriptionModalOpen(false)}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default UserProfileAvatar
|