src/features/account/account-shell.tsx
Rama main · réplica de lectura del repositorio original.
import './account-redesign.css'
import type { DeveloperConsoleAccessResponse } from '@shared/api/account'
import type { SiteNavigation } from '@shared/api/navigation'
import { useQueryClient } from '@tanstack/react-query'
import { Link, useNavigate } from '@tanstack/react-router'
import {
AppWindow,
ArrowUpRight,
BookOpen,
Bot,
Building2,
Database,
Folder,
HelpCircle,
LayoutDashboard,
Link2,
LoaderCircle,
Menu,
UserRound,
Wallet,
} from 'lucide-react'
import { type ReactNode, useRef, useState } from 'react'
import { toast } from 'sonner'
import { brandingStyle } from '@/components/layout/auth-layout'
import { ProductAccountMenu } from '@/components/product-account-menu'
import { RealmrootWordmark } from '@/components/realmroot-brand'
import { Button } from '@/components/ui/button'
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
import { Status } from '@/components/ui/status'
import { signOut } from '@/lib/auth-client'
import { tt } from '@/lib/i18n'
import { cn } from '@/lib/utils'
import { OrganizationWorkspaceNavigation } from './organization-workspace-navigation'
import type { AccountCenterSection, defaultAccountCenterSettings } from './settings'
import type { UserProfile } from './types'
type AccountCenterSettings = typeof defaultAccountCenterSettings
const accountNavigation = [
{ section: 'overview', href: '/', label: 'Workbench', icon: LayoutDashboard },
{ section: 'agents', href: '/agents', label: 'Agents', icon: Bot },
{ section: 'applications', href: '/applications', label: 'Access management', icon: Link2 },
{ section: 'profile', href: '/profile', label: 'Account settings', icon: UserRound },
{ section: 'data-privacy', href: '/data-privacy', label: 'Data & privacy', icon: Database },
] as const
export function AccountPageShell({
access,
accountCenter,
children,
config,
organizationId,
pathname = window.location.pathname,
profile,
section,
}: {
access: DeveloperConsoleAccessResponse
accountCenter: AccountCenterSettings
children: ReactNode
config: Parameters<typeof brandingStyle>[0]
organizationId?: string
pathname?: string
profile: UserProfile
section: AccountCenterSection
}) {
void accountCenter
const navigate = useNavigate()
const queryClient = useQueryClient()
const [navigationOpen, setNavigationOpen] = useState(false)
const navigationTriggerRef = useRef<HTMLButtonElement>(null)
const productName = organizationId ? 'Developer Center' : 'Account Center'
function changeNavigation(open: boolean) {
setNavigationOpen(open)
if (!open) window.setTimeout(() => navigationTriggerRef.current?.focus(), 0)
}
async function signOutFromAccount() {
try {
await signOut()
queryClient.clear()
toast.success(tt('Signed out'))
await navigate({ to: '/auth/sign-in' })
} catch (mutationError) {
toast.error(mutationError instanceof Error ? tt(mutationError.message) : tt('Account update failed.'))
}
}
return (
<main className={cn('accountShell', !organizationId && 'accountRedesign')} style={brandingStyle(config)}>
<a className="skipLink" href="#account-content">
{tt('Skip to content')}
</a>
<header className="accountProductTopbar">
<div className="flex min-w-0 items-center gap-2">
<Button
aria-expanded={navigationOpen}
aria-label={tt(organizationId ? 'Open Developer Center navigation' : 'Open Account Center navigation')}
className="accountMobileMenu"
onClick={() => setNavigationOpen(true)}
ref={navigationTriggerRef}
size="icon"
variant="ghost"
>
<Menu />
</Button>
{organizationId ? (
<Link
aria-label={tt('Developer Center home')}
params={{ organizationId }}
to="/organizations/$organizationId/overview"
>
<RealmrootWordmark context={tt(productName)} />
</Link>
) : (
<Link aria-label={tt('Account Center home')} to="/">
<span className="accountBreadcrumb">
{tt('Account Center')} <span aria-hidden="true">›</span>{' '}
{tt(
accountNavigation.find(
(item) =>
item.section ===
(section === 'security' ? 'profile' : section === 'connections' ? 'applications' : section),
)?.label ?? 'Developer Center',
)}
</span>
</Link>
)}
</div>
<div className="flex items-center gap-2">
<Button
asChild
aria-label={tt('Help & documentation')}
className="hidden sm:inline-flex"
size="icon"
variant="ghost"
>
<a href="https://github.com/realmroot/realmroot/tree/main/docs" rel="noreferrer" target="_blank">
<HelpCircle />
</a>
</Button>
<ProductAccountMenu
onSignOut={() => void signOutFromAccount()}
primaryAction={
access.platformOperator ? { icon: LayoutDashboard, label: 'Console', to: '/console' } : undefined
}
profile={profile}
/>
</div>
</header>
<div className="accountShellLayout">
{organizationId ? (
<OrganizationWorkspaceNavigation organizationId={organizationId} pathname={pathname} />
) : (
<AccountSidebar
access={access}
externalLinks={config?.navigation?.externalLinks ?? []}
profile={profile}
section={section}
/>
)}
<section
className={cn(
'accountContent',
(section === 'profile' || section === 'security' || section === 'data-privacy') && 'is-settings',
organizationId && 'is-workspace',
)}
id="account-content"
tabIndex={-1}
>
{children}
</section>
</div>
<Sheet onOpenChange={changeNavigation} open={navigationOpen}>
<SheetContent
className="accountMobileNavSheet w-72 p-0"
onCloseAutoFocus={(event) => {
event.preventDefault()
navigationTriggerRef.current?.focus()
}}
side="left"
>
<SheetHeader className="border-b">
<SheetTitle>{tt(productName)}</SheetTitle>
<SheetDescription className="sr-only">
{tt(organizationId ? 'Navigate Developer Center pages.' : 'Navigate Account Center pages.')}
</SheetDescription>
</SheetHeader>
{organizationId ? (
<OrganizationWorkspaceNavigation
onNavigate={() => setNavigationOpen(false)}
organizationId={organizationId}
pathname={pathname}
/>
) : (
<AccountSidebar
access={access}
externalLinks={config?.navigation?.externalLinks ?? []}
onNavigate={() => setNavigationOpen(false)}
profile={profile}
section={section}
/>
)}
</SheetContent>
</Sheet>
</main>
)
}
export function AccountPageLoading({ config }: { config: Parameters<typeof brandingStyle>[0] }) {
return (
<main className="accountShell" style={brandingStyle(config)}>
<header className="accountProductTopbar">
<RealmrootWordmark context={tt('Account Center')} />
</header>
<section className="accountStandaloneState">
<Status>
<LoaderCircle className="spin" size={18} />
{tt('Loading account center')}
</Status>
</section>
</main>
)
}
export function AccountPageError({
config,
message,
}: {
config: Parameters<typeof brandingStyle>[0]
message: string
}) {
return (
<main className="accountShell" style={brandingStyle(config)}>
<header className="accountProductTopbar">
<RealmrootWordmark context={tt('Account Center')} />
</header>
<section className="accountStandaloneState">
<Status tone="error">{message}</Status>
</section>
</main>
)
}
const externalLinkIcons = { wallet: Wallet, app: AppWindow, link: Link2, book: BookOpen, folder: Folder }
function AccountSidebar({
access,
externalLinks,
onNavigate,
profile,
section,
}: {
access: DeveloperConsoleAccessResponse
externalLinks: SiteNavigation['externalLinks']
onNavigate?: () => void
profile: UserProfile
section: AccountCenterSection
}) {
void profile
const activeSection = section === 'security' ? 'profile' : section === 'connections' ? 'applications' : section
const organizationId = access.consoleOrganizations[0]?.organizationId
return (
<aside className="accountSidebar">
<Link className="accountSidebarBrand" aria-label={tt('Account Center home')} to="/">
<RealmrootWordmark />
</Link>
<nav aria-label={tt('Account Center')} className="accountNav">
{accountNavigation.map((item) => (
<Link
key={item.section}
to={item.href}
onClick={onNavigate}
aria-current={activeSection === item.section ? 'page' : undefined}
className={cn('accountNavItem', activeSection === item.section && 'is-active')}
>
<item.icon aria-hidden="true" />
<span>{tt(item.label)}</span>
</Link>
))}
{externalLinks.length > 0 ? (
<div className="accountNavGroup">
<p>{tt('More services')}</p>
{externalLinks.map((link) => {
const Icon = externalLinkIcons[link.icon]
return (
<a className="accountNavItem" href={link.url} key={link.id} onClick={onNavigate}>
<Icon aria-hidden="true" />
<span>{link.label}</span>
<ArrowUpRight aria-hidden="true" className="ml-auto size-3" />
</a>
)
})}
</div>
) : null}
</nav>
{access.showOrganizations || access.canCreateOrganization ? (
<div className="accountDeveloperEntry">
{organizationId ? (
<Link
className="accountNavItem"
onClick={onNavigate}
to="/organizations/$organizationId/overview"
params={{ organizationId }}
>
<Building2 />
<span>{tt('Developer Center')}</span>
</Link>
) : (
<Link className="accountNavItem" onClick={onNavigate} to="/organizations">
<Building2 />
<span>{tt('Developer Center')}</span>
</Link>
)}
</div>
) : null}
</aside>
)
}