analytica-frontend-lib
Version:
Repositório público dos componentes utilizados nas plataformas da Analytica Ensino
1 lines • 18.3 kB
Source Map (JSON)
{"version":3,"sources":["../src/components/AppHeader/AppHeader.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { CaretRightIcon } from '@phosphor-icons/react/dist/csr/CaretRight';\nimport { UserIcon as UserPhosphorIcon } from '@phosphor-icons/react/dist/csr/User';\n\nimport { BrandingLogo } from '../BrandingLogo/BrandingLogo';\nimport CalendarCard from '../CalendarCard/CalendarCard';\nimport IconButton from '../IconButton/IconButton';\nimport DropdownMenu, {\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n ProfileMenuFooter,\n ProfileMenuHeader,\n ProfileMenuInfo,\n ProfileMenuSection,\n ProfileToggleTheme,\n} from '../DropdownMenu/DropdownMenu';\nimport NotificationCard, {\n type NotificationGroup,\n} from '../NotificationCard/NotificationCard';\nimport { PageContainer } from '../PageContainer/PageContainer';\nimport Text from '../Text/Text';\nimport { UserIcon } from '../UserIcon/UserIcon';\nimport { useMobile } from '../../hooks/useMobile';\nimport { syncDropdownState } from '../../utils/dropdown';\nimport { NotificationEntityType } from '../../types/notifications';\n\n/**\n * Subset of session info consumed by the application header.\n * Each property is optional so the consumer can pass whatever it has.\n */\nexport interface AppHeaderSessionInfo {\n userName?: string;\n email?: string;\n urlProfilePicture?: string;\n schoolName?: string;\n className?: string;\n schoolYearName?: string;\n}\n\n/**\n * Minimal user shape needed to fill the profile dropdown header.\n */\nexport interface AppHeaderUser {\n name?: string;\n email?: string;\n}\n\n/**\n * Tutorial action rendered as a pill button in the header (left of the\n * notifications bell). The consumer owns the click behavior (usually opening\n * the configured tutorial URL in a new tab).\n */\nexport interface AppHeaderTutorial {\n /** Show the tutorial button. */\n visible?: boolean;\n /** Button label. Defaults to \"Tutoriais\". */\n label?: string;\n /** Fired when the tutorial button is clicked. */\n onClick: () => void;\n}\n\n/**\n * Notifications data + callbacks the AppHeader forwards to NotificationCard.\n * Built by the consumer (usually via `createNotificationsHook(api)`).\n */\nexport interface AppHeaderNotifications {\n unreadCount: number;\n loading: boolean;\n error: string | null;\n groupedNotifications: NotificationGroup[];\n refreshNotifications: () => void;\n markAsRead: (id: string) => void;\n deleteNotification: (id: string) => void;\n fetchNotifications: () => void;\n fetchUnreadCount?: () => void;\n getActionLabel: (entityType?: NotificationEntityType) => string | undefined;\n}\n\nexport interface AppHeaderProps {\n /** Authenticated user (fallback when sessionInfo doesn't carry name/email). */\n user?: AppHeaderUser;\n /** Session metadata used to render profile name, email, photo and school info. */\n sessionInfo?: AppHeaderSessionInfo;\n /** Notifications wiring (state + callbacks). */\n notifications: AppHeaderNotifications;\n /** Tutorial pill button shown before the notifications bell. */\n tutorial?: AppHeaderTutorial;\n /** Show the calendar widget in the header. */\n showCalendar?: boolean;\n /** Content rendered inside the calendar (dropdown on tablet/desktop, modal on mobile). */\n calendarContent?: ReactNode;\n /**\n * Optional controlled open state for the calendar.\n * When provided, the consumer is the source of truth; useful for closing\n * the calendar programmatically (e.g. on route change).\n */\n calendarOpen?: boolean;\n /** Fired whenever the calendar open state changes. */\n onCalendarOpenChange?: (open: boolean) => void;\n /** Show school/class/year inside the profile dropdown. */\n showProfileInfo?: boolean;\n /** Optional fallback image for the notifications empty state. */\n emptyNotificationsImage?: string;\n /** Empty-state title for notifications dropdown. */\n emptyNotificationsTitle?: string;\n /** Empty-state description for notifications dropdown. */\n emptyNotificationsDescription?: string;\n /** Callback fired when the user activates logout. */\n onLogout: () => void;\n /** Callback fired when the user activates the \"My data\" menu item. */\n onNavigateToMyData: () => void;\n /** Callback fired when the user clicks on a notification with entity info. */\n onNavigateByNotification?: (\n entityType?: NotificationEntityType,\n entityId?: string\n ) => void;\n /**\n * Tailwind class overriding the default `max-w-[1000px]` of the header\n * content (forwarded to the internal `PageContainer`'s `innerClassName`).\n * Pass e.g. `max-w-[1350px]` to align the header with a widened page\n * content. When omitted, the default `max-w-[1000px]` is kept, so other\n * consumers are unaffected.\n */\n contentMaxWidth?: string;\n}\n\nconst DEFAULT_EMPTY_TITLE = 'Nenhuma notificação no momento';\nconst DEFAULT_EMPTY_DESCRIPTION =\n 'Você está em dia com todas as novidades. Volte depois para conferir atualizações!';\n\n/**\n * Application header shared across the analytica frontends.\n *\n * Renders the institution logo, optional calendar dropdown (mobile/tablet),\n * notifications center and a profile menu. Width is constrained by\n * `<PageContainer>` so it always aligns with the rest of the page content.\n */\nexport const AppHeader = ({\n user,\n sessionInfo,\n notifications,\n tutorial,\n showCalendar = false,\n calendarContent,\n calendarOpen,\n onCalendarOpenChange,\n showProfileInfo = false,\n emptyNotificationsImage,\n emptyNotificationsTitle = DEFAULT_EMPTY_TITLE,\n emptyNotificationsDescription = DEFAULT_EMPTY_DESCRIPTION,\n onLogout,\n onNavigateToMyData,\n onNavigateByNotification,\n contentMaxWidth,\n}: AppHeaderProps) => {\n const { isMobile, isExtraSmallMobile } = useMobile();\n const [activeStates, setActiveStates] = useState<Record<string, boolean>>({});\n\n useEffect(() => {\n notifications.fetchUnreadCount?.();\n }, []);\n\n const toggleActive = (buttonId: string) => {\n setActiveStates((prev) => {\n if (prev[buttonId]) {\n return { ...prev, [buttonId]: false };\n }\n return {\n calendar: false,\n notifications: false,\n profile: false,\n [buttonId]: true,\n };\n });\n };\n\n // Stable callback so `DropdownMenu`'s `useEffect [open, onOpenChange]`\n // doesn't re-fire on every parent re-render, which would cascade into a\n // Maximum-update-depth loop (same root cause class as PR #435).\n const handleCalendarOpenChange = useCallback(\n (open: boolean) => {\n setActiveStates((prev) => {\n const current = prev.calendar ?? false;\n if (current === open) {\n return prev;\n }\n if (open) {\n return {\n calendar: true,\n notifications: false,\n profile: false,\n };\n }\n return { ...prev, calendar: false };\n });\n onCalendarOpenChange?.(open);\n },\n [onCalendarOpenChange]\n );\n\n useEffect(() => {\n const handleEscapeKey = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n setActiveStates({});\n }\n };\n document.addEventListener('keydown', handleEscapeKey);\n return () => document.removeEventListener('keydown', handleEscapeKey);\n }, []);\n\n const handleMyDataClick = () => {\n setActiveStates({});\n onNavigateToMyData();\n };\n\n const displayName = sessionInfo?.userName ?? user?.name ?? 'Usuário';\n const displayEmail = sessionInfo?.email ?? user?.email ?? '';\n\n const pickResponsiveClass = (\n extraSmall: string,\n mobile: string,\n desktop: string\n ): string => {\n if (isExtraSmallMobile) return extraSmall;\n if (isMobile) return mobile;\n return desktop;\n };\n const profileDropdownWidth = pickResponsiveClass(\n 'min-w-[260px]',\n 'min-w-[280px]',\n 'min-w-[320px]'\n );\n const sectionGap = pickResponsiveClass('gap-1', 'gap-2', 'gap-3');\n const logoHeight = pickResponsiveClass('h-6', 'h-8', 'h-10');\n const tutorialPadding = pickResponsiveClass(\n 'px-3 py-1.5 text-xs',\n 'px-3.5 py-2 text-sm',\n 'px-4 py-2 text-sm'\n );\n\n return (\n <header\n data-component=\"AppHeader\"\n className=\"bg-primary-800 w-full h-[70px] flex justify-center items-center\"\n >\n <PageContainer\n className=\"pb-0 justify-center\"\n innerClassName={contentMaxWidth}\n >\n <div className=\"w-full flex flex-row justify-between items-center gap-3\">\n {/* `min-w-0 shrink` lets the logo compress instead of overflowing:\n an <img> resolves `min-width:auto` to its intrinsic width, which\n would otherwise block flex-shrink and push the actions off-screen\n on narrow viewports. `object-left` keeps it left-aligned as it\n scales down. The actions never shrink. */}\n <BrandingLogo\n variant=\"main\"\n alt=\"Logo\"\n className={`${logoHeight} object-contain object-left min-w-0 shrink`}\n />\n <section\n className={`flex flex-row items-center shrink-0 ${sectionGap}`}\n >\n {tutorial?.visible && (\n <button\n type=\"button\"\n data-testid=\"app-header-tutorial\"\n onClick={tutorial.onClick}\n /* `primary` (não `text`) é o foreground de `primary-800` em\n todos os temas — os dois formam um par contrastante mesmo\n onde o dark inverte o header (paraiba/analytica). Usar\n `text-*` aqui inverteria com o modo enquanto o fundo do\n header não inverte, apagando o botão no dark. */\n className={`rounded-full border border-primary text-primary font-medium whitespace-nowrap cursor-pointer transition-colors hover:bg-primary-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary ${tutorialPadding}`}\n >\n {tutorial.label ?? 'Tutoriais'}\n </button>\n )}\n\n {showCalendar && (\n <div className=\"lg:hidden\">\n <CalendarCard\n content={calendarContent}\n isOpen={calendarOpen ?? activeStates.calendar ?? false}\n onOpenChange={handleCalendarOpenChange}\n />\n </div>\n )}\n\n <NotificationCard\n mode=\"center\"\n isActive={activeStates.notifications}\n onToggleActive={() => toggleActive('notifications')}\n onOpenChange={(open: boolean) => {\n syncDropdownState(\n open,\n activeStates.notifications,\n setActiveStates,\n 'notifications'\n );\n }}\n unreadCount={notifications.unreadCount}\n groupedNotifications={notifications.groupedNotifications}\n loading={notifications.loading}\n error={notifications.error}\n onRetry={notifications.refreshNotifications}\n onMarkAsReadById={notifications.markAsRead}\n onDeleteById={notifications.deleteNotification}\n onNavigateById={(entityType, entityId) => {\n if (entityType && entityId) {\n onNavigateByNotification?.(entityType, entityId);\n }\n }}\n getActionLabel={notifications.getActionLabel}\n onFetchNotifications={notifications.fetchNotifications}\n emptyStateImage={emptyNotificationsImage}\n emptyStateTitle={emptyNotificationsTitle}\n emptyStateDescription={emptyNotificationsDescription}\n />\n\n <DropdownMenu\n onOpenChange={(open: boolean) => {\n syncDropdownState(\n open,\n activeStates.profile,\n setActiveStates,\n 'profile'\n );\n }}\n >\n <DropdownMenuTrigger className=\"text-primary cursor-pointer\">\n <IconButton\n active={activeStates.profile}\n onClick={() => toggleActive('profile')}\n icon={\n <UserIcon\n size={24}\n className={activeStates.profile ? 'opacity-80' : ''}\n />\n }\n />\n </DropdownMenuTrigger>\n <DropdownMenuContent\n className={profileDropdownWidth}\n side=\"bottom\"\n variant=\"profile\"\n align=\"end\"\n >\n <ProfileMenuHeader\n name={displayName}\n email={displayEmail}\n photoUrl={sessionInfo?.urlProfilePicture}\n />\n\n {showProfileInfo && (\n <ProfileMenuInfo\n schoolName={sessionInfo?.schoolName ?? ''}\n classYearName={sessionInfo?.className ?? ''}\n schoolYearName={sessionInfo?.schoolYearName ?? ''}\n />\n )}\n\n <ProfileMenuSection>\n <DropdownMenuItem\n variant=\"profile\"\n iconLeft={<UserPhosphorIcon />}\n iconRight={<CaretRightIcon />}\n onClick={handleMyDataClick}\n >\n <Text size=\"md\" color=\"text-text-700\">\n Meus dados\n </Text>\n </DropdownMenuItem>\n\n <ProfileToggleTheme />\n </ProfileMenuSection>\n\n <ProfileMenuFooter onClick={onLogout} />\n </DropdownMenuContent>\n </DropdownMenu>\n </section>\n </div>\n </PageContainer>\n </header>\n );\n};\n\nexport default AppHeader;\n"],"mappings":"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","names":[]}