UserDropdown.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. import { useFlag } from 'common'
  2. import { FlaskConical, Loader2, ScrollText, Settings } from 'lucide-react'
  3. import { useTheme } from 'next-themes'
  4. import Link from 'next/link'
  5. import { useRouter } from 'next/router'
  6. import {
  7. cn,
  8. DropdownMenu,
  9. DropdownMenuContent,
  10. DropdownMenuGroup,
  11. DropdownMenuItem,
  12. DropdownMenuLabel,
  13. DropdownMenuRadioGroup,
  14. DropdownMenuRadioItem,
  15. DropdownMenuSeparator,
  16. DropdownMenuTrigger,
  17. singleThemes,
  18. Theme,
  19. } from 'ui'
  20. import { ButtonTooltip } from '../ui/ButtonTooltip'
  21. import { useFeaturePreviewModal } from './App/FeaturePreview/FeaturePreviewContext'
  22. import { TimezoneDropdown } from './UserDropdown/TimezoneDropdown'
  23. import { ProfileImage } from '@/components/ui/ProfileImage'
  24. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  25. import { IS_PLATFORM } from '@/lib/constants'
  26. import { useProfileNameAndPicture } from '@/lib/profile'
  27. import { useTrack } from '@/lib/telemetry/track'
  28. import { useAppStateSnapshot } from '@/state/app-state'
  29. export function UserDropdown({
  30. triggerClassName,
  31. contentClassName,
  32. }: {
  33. triggerClassName?: string
  34. contentClassName?: string
  35. }) {
  36. const router = useRouter()
  37. const { theme, setTheme } = useTheme()
  38. const appStateSnapshot = useAppStateSnapshot()
  39. const profileShowEmailEnabled = useIsFeatureEnabled('profile:show_email')
  40. const timezonePickerEnabled = useFlag('timezonePicker')
  41. const { username, avatarUrl, primaryEmail, isLoading } = useProfileNameAndPicture()
  42. const { toggleFeaturePreviewModal } = useFeaturePreviewModal()
  43. const track = useTrack()
  44. return (
  45. <DropdownMenu
  46. onOpenChange={(open) => {
  47. if (open) track('header_user_dropdown_opened')
  48. }}
  49. >
  50. <DropdownMenuTrigger asChild className={cn('border shrink-0 px-3', triggerClassName)}>
  51. <ButtonTooltip
  52. type="default"
  53. className="[&>span]:flex px-0 py-0 rounded-full overflow-hidden h-8 w-8"
  54. tooltip={{ content: { text: 'Account settings' } }}
  55. >
  56. {isLoading ? (
  57. <div className="w-full h-full flex items-center justify-center">
  58. <Loader2 className="animate-spin text-foreground-lighter" size={16} />
  59. </div>
  60. ) : (
  61. <ProfileImage alt={username} src={avatarUrl} className="w-8 h-8 rounded-md" />
  62. )}
  63. </ButtonTooltip>
  64. </DropdownMenuTrigger>
  65. <DropdownMenuContent side="bottom" align="end" className={contentClassName}>
  66. {IS_PLATFORM && (
  67. <>
  68. <div className="px-2 py-1 flex flex-col gap-0 text-sm">
  69. {!!username ? (
  70. <>
  71. <span title={username} className="w-full text-left text-foreground truncate">
  72. {username}
  73. </span>
  74. {primaryEmail !== username && profileShowEmailEnabled && (
  75. <span
  76. title={primaryEmail}
  77. className="w-full text-left text-foreground-light text-xs truncate"
  78. >
  79. {primaryEmail}
  80. </span>
  81. )}
  82. </>
  83. ) : (
  84. <span title={primaryEmail} className="w-full text-left text-foreground truncate">
  85. {primaryEmail}
  86. </span>
  87. )}
  88. </div>
  89. <DropdownMenuSeparator />
  90. <DropdownMenuGroup>
  91. <DropdownMenuItem className="flex gap-2 cursor-pointer" asChild>
  92. <Link
  93. href="/account/me"
  94. onClick={() => {
  95. if (router.pathname !== '/account/me') {
  96. appStateSnapshot.setLastRouteBeforeVisitingAccountPage(router.asPath)
  97. }
  98. }}
  99. >
  100. <Settings size={14} strokeWidth={1.5} className="text-foreground-lighter" />
  101. Account preferences
  102. </Link>
  103. </DropdownMenuItem>
  104. <DropdownMenuItem
  105. className="flex gap-2 cursor-pointer"
  106. onClick={() => toggleFeaturePreviewModal(true)}
  107. // onSelect={() => toggleFeaturePreviewModal(true)}
  108. >
  109. <FlaskConical size={14} strokeWidth={1.5} className="text-foreground-lighter" />
  110. Feature previews
  111. </DropdownMenuItem>
  112. <DropdownMenuItem className="flex gap-2 cursor-pointer" asChild>
  113. <Link
  114. href="https://supabase.com/changelog"
  115. target="_blank"
  116. rel="noopener noreferrer"
  117. >
  118. <ScrollText size={14} strokeWidth={1.5} className="text-foreground-lighter" />
  119. Changelog
  120. </Link>
  121. </DropdownMenuItem>
  122. <DropdownMenuSeparator />
  123. </DropdownMenuGroup>
  124. </>
  125. )}
  126. <DropdownMenuGroup>
  127. <DropdownMenuLabel>Theme</DropdownMenuLabel>
  128. <DropdownMenuRadioGroup
  129. value={theme}
  130. onValueChange={(value) => {
  131. setTheme(value)
  132. }}
  133. >
  134. {singleThemes.map((theme: Theme) => (
  135. <DropdownMenuRadioItem
  136. key={theme.value}
  137. value={theme.value}
  138. className="cursor-pointer"
  139. >
  140. {theme.name}
  141. </DropdownMenuRadioItem>
  142. ))}
  143. </DropdownMenuRadioGroup>
  144. </DropdownMenuGroup>
  145. {timezonePickerEnabled && (
  146. <>
  147. <DropdownMenuSeparator />
  148. <DropdownMenuGroup>
  149. <TimezoneDropdown />
  150. </DropdownMenuGroup>
  151. </>
  152. )}
  153. {IS_PLATFORM && (
  154. <>
  155. <DropdownMenuSeparator />
  156. <DropdownMenuGroup>
  157. <DropdownMenuItem
  158. className="cursor-pointer"
  159. onSelect={() => {
  160. router.push('/logout')
  161. }}
  162. >
  163. Log out
  164. </DropdownMenuItem>
  165. </DropdownMenuGroup>
  166. </>
  167. )}
  168. </DropdownMenuContent>
  169. </DropdownMenu>
  170. )
  171. }