import { formatPlanPrice } from '@shared/pricing'; import { IconAlertCircle as AlertCircle, IconAlertTriangle as AlertTriangle, IconArrowRight as ArrowRight, IconBell as Bell, IconBook as Book, IconBuildingSkyscraper as Building, IconCircleCheck as CircleCheck, IconCreditCard as CreditCard, IconInfoCircle as InfoCircle, IconPlus as Plus, IconShieldCheck as ShieldCheck, IconShieldLock as ShieldLock, IconSparkles as Sparkles, IconUser as User, IconUsers as Users, } from '@tabler/icons-react'; import type { ComponentType, ReactNode } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router'; import { useAuth } from '../components/auth/AuthProvider'; import { ContentPanel } from '../components/ContentPanel'; import { PageHeader } from '../components/PageHeader'; import { Badge } from '../components/ui/badge'; import { Button } from '../components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card'; import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, } from '../components/ui/empty'; import { Skeleton } from '../components/ui/skeleton'; import { useDocsVisibility } from '../hooks/api'; import { useSubscription } from '../hooks/api/useSubscription'; import type { Notification, NotificationType } from '../hooks/useNotifications'; import { useNotifications } from '../hooks/useNotifications'; import { type Organization, useOrganizations } from '../hooks/useOrganizations'; import { cn, getUserInitials } from '../lib/utils'; function getGreetingKey() { const hour = new Date().getHours(); if (hour < 12) return 'dashboard.greeting.morning'; if (hour < 17) return 'dashboard.greeting.afternoon'; return 'dashboard.greeting.evening'; } function relativeTime(iso: string): string { const then = new Date(iso).getTime(); if (Number.isNaN(then)) return ''; const diff = Date.now() - then; const mins = Math.round(diff / 60000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins}m ago`; const hours = Math.round(mins / 60); if (hours < 24) return `${hours}h ago`; const days = Math.round(hours / 24); if (days < 30) return `${days}d ago`; return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } const NOTIFICATION_ICON: Record> = { info: InfoCircle, success: CircleCheck, warning: AlertTriangle, error: AlertCircle, }; const NOTIFICATION_TONE: Record = { info: 'text-primary', success: 'text-success', warning: 'text-warning', error: 'text-destructive', }; function StatCard({ icon: Icon, label, value, hint, }: { icon: ComponentType<{ className?: string }>; label: string; value: ReactNode; hint?: ReactNode; }) { return (
{label}

{value}

{hint &&

{hint}

}
); } function OrgRow({ org, role }: { org: Organization; role?: string }) { return (
{org.name.slice(0, 1).toUpperCase()}

{org.name}

/{org.slug}

{role && ( {role.toLowerCase()} )} ); } function NotificationRow({ notification }: { notification: Notification }) { const Icon = NOTIFICATION_ICON[notification.type] ?? InfoCircle; const tone = NOTIFICATION_TONE[notification.type] ?? 'text-primary'; return (

{notification.title}

{notification.message && (

{notification.message}

)}
{relativeTime(notification.createdAt)}
); } function QuickAction({ to, icon: Icon, label, }: { to: string; icon: ComponentType<{ className?: string }>; label: string; }) { return (
{label} ); } export default function Dashboard() { const { t } = useTranslation(); const { user } = useAuth(); const { organizations, memberships, selectedOrganization, selectedOrganizationRole, isLoading: orgsLoading, } = useOrganizations(); const { plan, isActive, isCanceling, subscription } = useSubscription(); const { notifications, isLoading: notifsLoading } = useNotifications(); const { userDocsEnabled } = useDocsVisibility(); const greeting = t(getGreetingKey()); const initials = getUserInitials(user); const firstName = user?.user_metadata?.full_name?.split(' ')[0] || user?.email?.split('@')[0] || 'there'; const formattedDate = new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', }); const roleForOrg = (orgId: string) => memberships.find((m) => m.organization.id === orgId)?.role; const isFree = plan.id === 'free'; const planStatus = isActive ? 'Active' : 'Inactive'; const renewsOn = subscription?.currentPeriodEnd && new Date(subscription.currentPeriodEnd).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); return ( <> {/* Welcome banner */}
{initials}
{greeting}, {firstName}! {user?.email}
{plan.name}

{formattedDate}

{/* User-scoped stats */}
{selectedOrganizationRole.toLowerCase()} ) : ( '—' ) } hint={selectedOrganization?.name ?? 'No active org'} />
{/* Organizations + activity */}
Your organizations Teams and workspaces you belong to
{orgsLoading ? (
) : organizations.length === 0 ? ( No organizations yet Create an organization to invite teammates and manage billing together. ) : (
{organizations.map((org) => ( ))}
)}
Recent activity Notifications and updates for your account {notifsLoading ? (
) : notifications.length === 0 ? ( You're all caught up New notifications will show up here. ) : (
{notifications.slice(0, 6).map((n) => ( ))}
)}
{/* Your plan */}

{plan.name} plan

{!isFree && ( {planStatus} )}

{isFree ? 'Upgrade for higher limits and team features.' : isCanceling && renewsOn ? `Cancels on ${renewsOn}` : renewsOn ? `Renews ${renewsOn} · ${formatPlanPrice(plan)}` : formatPlanPrice(plan)}

{/* Quick actions */}

Quick actions

{userDocsEnabled && }
); }