mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
Inline style objects became gui props (Box for a div, View or Text rendering the element for the rest, since Box styles only a div), raw colours and sizes became the design's tokens, the brand swatches read @hanzo/design's own values, and the project and creation hues moved to palette.json, which is data. The newsletter's subscribe button is the standard filled Action. connect-steps.tsx, which nothing imports, is deleted with the brand constants nobody read. Pages compared before and after at 1440x900 and 390x844: blockchain, brand, directory, projects, settings, unsubscribe identical; founder 0.03% of pixels; buying specialist's title snaps from 28 to the ramp's 26px and the newsletter's button is now filled. Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
255 lines
8.8 KiB
TypeScript
255 lines
8.8 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import {
|
|
Check,
|
|
CreditCard,
|
|
ExternalLink,
|
|
HelpCircle,
|
|
Languages,
|
|
LogOut,
|
|
Search,
|
|
Settings,
|
|
Sparkles,
|
|
User,
|
|
Users,
|
|
} from 'lucide-react'
|
|
import {
|
|
Avatar,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuSub,
|
|
DropdownMenuSubContent,
|
|
DropdownMenuSubTrigger,
|
|
DropdownMenuTrigger,
|
|
Input,
|
|
Text,
|
|
View,
|
|
XStack,
|
|
YStack,
|
|
} from '@hanzo/ui'
|
|
import { rememberDestination } from '@hanzo/app/lib/destination'
|
|
import { locales } from '@hanzo/i18n'
|
|
import { useI18n } from '@hanzo/i18n/react'
|
|
|
|
export interface AccountAdminWidgetProps {
|
|
person: {
|
|
name?: string
|
|
email?: string
|
|
avatar?: string
|
|
}
|
|
onSignOut: () => void | Promise<void>
|
|
}
|
|
|
|
export function AccountAdminWidget({ person, onSignOut }: AccountAdminWidgetProps) {
|
|
const { t, dir, locale, setLocale } = useI18n()
|
|
const [langOpen, setLangOpen] = useState(false)
|
|
const [langQuery, setLangQuery] = useState('')
|
|
const name = person.name || person.email || t('Account')
|
|
const email = person.email || ''
|
|
const avatar = person.avatar || null
|
|
const q = langQuery.trim().toLowerCase()
|
|
const filteredLocales = locales
|
|
.filter((l) => !l.review || l.code === locale)
|
|
.filter((l) => !q || l.name.toLowerCase().includes(q) || l.englishName.toLowerCase().includes(q) || l.code.toLowerCase().includes(q))
|
|
|
|
const handleUseAnotherAccount = () => {
|
|
rememberDestination()
|
|
void onSignOut()
|
|
}
|
|
|
|
const handleLogOut = () => {
|
|
void onSignOut()
|
|
}
|
|
|
|
return (
|
|
<XStack render="section" aria-label={t('Account')} items="center">
|
|
<DropdownMenu placement={dir === 'rtl' ? 'bottom-start' : 'bottom-end'} offset={8}>
|
|
<DropdownMenuTrigger asChild>
|
|
<XStack
|
|
render={<button type="button" />}
|
|
aria-label={t('Account: {name}', { name })}
|
|
items="center"
|
|
gap={8}
|
|
p={2}
|
|
pr={8}
|
|
rounded={999}
|
|
borderWidth={1}
|
|
borderColor="var(--border)"
|
|
bg="transparent"
|
|
hoverStyle={{ borderColor: 'var(--foreground)', bg: '$hover' }}
|
|
cursor="pointer"
|
|
$platform-web={{ transition: 'border-color 160ms, background-color 160ms' }}
|
|
>
|
|
<Avatar src={avatar} name={name} alt="" width={30} height={30} rounded={999} />
|
|
<Text
|
|
fontSize="$2"
|
|
fontWeight="500"
|
|
color="var(--foreground)"
|
|
numberOfLines={1}
|
|
maxW={120}
|
|
display="none"
|
|
$md={{ display: 'flex' }}
|
|
>
|
|
{name.split(' ')[0]}
|
|
</Text>
|
|
</XStack>
|
|
</DropdownMenuTrigger>
|
|
|
|
<DropdownMenuContent minW={240} maxW="calc(100vw - 16px)" aria-label={t('Account')}>
|
|
<DropdownMenuLabel>
|
|
<XStack items="center" gap={10} py={4}>
|
|
<Avatar src={avatar} name={name} alt="" width={36} height={36} rounded={999} />
|
|
<YStack minW={0} gap={2} flex={1}>
|
|
<Text fontSize="$3" fontWeight="600" color="$ink" numberOfLines={1}>
|
|
{name}
|
|
</Text>
|
|
{email ? (
|
|
<Text fontSize="$1" color="$soft" numberOfLines={1}>
|
|
{email}
|
|
</Text>
|
|
) : null}
|
|
</YStack>
|
|
</XStack>
|
|
</DropdownMenuLabel>
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
<DropdownMenuItem onSelect={() => window.location.assign('/settings')}>
|
|
<XStack items="center" gap={8} width="100%">
|
|
<Settings size={15} color="var(--muted-foreground)" aria-hidden />
|
|
<Text fontSize="$2" color="$ink">
|
|
{t('Settings & Profile')}
|
|
</Text>
|
|
</XStack>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem onSelect={() => window.location.assign('/beta')}>
|
|
<XStack items="center" gap={8} width="100%">
|
|
<Sparkles size={15} color="var(--muted-foreground)" aria-hidden />
|
|
<Text fontSize="$2" color="$ink">
|
|
{t('Beta Dashboard')}
|
|
</Text>
|
|
</XStack>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem onSelect={() => window.location.assign('/billing')}>
|
|
<XStack items="center" gap={8} width="100%">
|
|
<CreditCard size={15} color="var(--muted-foreground)" aria-hidden />
|
|
<Text fontSize="$2" color="$ink">
|
|
{t('Billing & Plans')}
|
|
</Text>
|
|
</XStack>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem onSelect={() => window.location.assign('/console')}>
|
|
<XStack items="center" justify="space-between" width="100%">
|
|
<XStack items="center" gap={8}>
|
|
<User size={15} color="var(--muted-foreground)" aria-hidden />
|
|
<Text fontSize="$2" color="$ink">
|
|
{t('Organization & Console')}
|
|
</Text>
|
|
</XStack>
|
|
<ExternalLink size={12} color="var(--muted-foreground)" aria-hidden />
|
|
</XStack>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
<DropdownMenuSub
|
|
open={langOpen}
|
|
onOpenChange={(next) => {
|
|
setLangOpen(next)
|
|
if (!next) setLangQuery('')
|
|
}}
|
|
>
|
|
<DropdownMenuSubTrigger>
|
|
<XStack items="center" gap={8} width="100%">
|
|
<Languages size={15} color="var(--muted-foreground)" aria-hidden />
|
|
<Text fontSize="$2" color="$ink">
|
|
{t('Language')}
|
|
</Text>
|
|
</XStack>
|
|
</DropdownMenuSubTrigger>
|
|
{langOpen ? (
|
|
<DropdownMenuSubContent minW={240} maxW="calc(100vw - 16px)" pointerEvents="auto">
|
|
<View px={6} py={4} onKeyDown={(e) => e.stopPropagation()}>
|
|
<XStack
|
|
items="center"
|
|
gap={6}
|
|
px={8}
|
|
height={30}
|
|
rounded={8}
|
|
borderWidth={1}
|
|
borderColor="var(--border)"
|
|
bg="transparent"
|
|
>
|
|
<Search size={13} color="var(--muted-foreground)" aria-hidden />
|
|
<Input
|
|
unstyled
|
|
value={langQuery}
|
|
onChangeText={setLangQuery}
|
|
placeholder={t('Search language...')}
|
|
aria-label={t('Search language...')}
|
|
fontSize="$2"
|
|
color="$ink"
|
|
placeholderTextColor="$soft"
|
|
flex={1}
|
|
minW={0}
|
|
height={28}
|
|
/>
|
|
</XStack>
|
|
</View>
|
|
<YStack maxH={260} overflowY="auto">
|
|
{filteredLocales.map((l) => (
|
|
<DropdownMenuItem key={l.code} onSelect={() => void setLocale(l.code)}>
|
|
<XStack items="center" justify="space-between" width="100%" gap={8}>
|
|
<Text fontSize="$2" color="$ink" numberOfLines={1} flex={1}>
|
|
{l.englishName !== l.name ? `${l.name} (${l.englishName})` : l.name}
|
|
</Text>
|
|
{l.code === locale ? <Check size={15} color="var(--foreground)" aria-label={t('Current')} /> : null}
|
|
</XStack>
|
|
</DropdownMenuItem>
|
|
))}
|
|
</YStack>
|
|
</DropdownMenuSubContent>
|
|
) : null}
|
|
</DropdownMenuSub>
|
|
|
|
<DropdownMenuItem onSelect={() => window.location.assign('/support')}>
|
|
<XStack items="center" gap={8} width="100%">
|
|
<HelpCircle size={15} color="var(--muted-foreground)" aria-hidden />
|
|
<Text fontSize="$2" color="$ink">
|
|
{t('Help & Support')}
|
|
</Text>
|
|
</XStack>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
<DropdownMenuItem onSelect={handleUseAnotherAccount}>
|
|
<XStack items="center" gap={8} width="100%">
|
|
<Users size={15} color="var(--muted-foreground)" aria-hidden />
|
|
<Text fontSize="$2" color="$ink">
|
|
{t('Use another account')}
|
|
</Text>
|
|
</XStack>
|
|
</DropdownMenuItem>
|
|
|
|
<DropdownMenuItem onSelect={handleLogOut}>
|
|
<XStack items="center" gap={8} width="100%">
|
|
<LogOut size={15} color="var(--destructive)" aria-hidden />
|
|
<Text fontSize="$2" color="var(--destructive)">
|
|
{t('Log out')}
|
|
</Text>
|
|
</XStack>
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</XStack>
|
|
)
|
|
}
|