AnnouncementBanner.tsx 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. import { useTranslation } from 'react-i18next';
  2. import { AlertTriangle, Megaphone } from 'lucide-react';
  3. import type { Announcement } from '../api/client';
  4. import { announcementText } from '../hooks/useAnnouncements';
  5. interface AnnouncementBannerProps {
  6. // Unread important/critical announcements, most severe first.
  7. items: Announcement[];
  8. // Opens the panel with this message expanded.
  9. onOpen: (id: string) => void;
  10. markRead: (id: string) => void;
  11. }
  12. /**
  13. * The strip above the page for an unread important or critical announcement.
  14. *
  15. * One at a time, the most severe; "+N more" opens the panel. "Got it" marks it
  16. * read on the server, so it stays gone on every browser this user signs in on.
  17. * Info-level messages never get a banner -- only the sidebar dot.
  18. */
  19. export function AnnouncementBanner({ items, onOpen, markRead }: AnnouncementBannerProps) {
  20. const { t, i18n } = useTranslation();
  21. const first = items[0];
  22. if (!first) return null;
  23. const text = announcementText(first, i18n.language);
  24. const critical = first.level === 'critical';
  25. const Icon = critical ? AlertTriangle : Megaphone;
  26. return (
  27. <div
  28. role={critical ? 'alert' : 'status'}
  29. className={`px-4 py-2 flex items-center gap-3 border-b text-sm ${
  30. critical
  31. ? 'bg-red-100 dark:bg-red-500/20 border-red-300 dark:border-red-500/30'
  32. : 'bg-amber-100 dark:bg-amber-500/20 border-amber-300 dark:border-amber-500/30'
  33. }`}
  34. >
  35. <Icon className={`w-4 h-4 flex-shrink-0 ${critical ? 'text-red-500' : 'text-amber-500'}`} />
  36. <div className="flex items-center gap-x-2 gap-y-1 flex-wrap min-w-0">
  37. <span className={`font-medium ${critical ? 'text-red-800 dark:text-red-200' : 'text-amber-800 dark:text-amber-200'}`}>
  38. {text.title}
  39. </span>
  40. <button
  41. onClick={() => onOpen(first.id)}
  42. className={`font-medium underline ${
  43. critical
  44. ? 'text-red-700 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300'
  45. : 'text-amber-700 dark:text-amber-400 hover:text-amber-900 dark:hover:text-amber-300'
  46. }`}
  47. >
  48. {items.length > 1
  49. ? t('announcements.readMoreCount', { count: items.length - 1 })
  50. : t('announcements.readMore')}
  51. </button>
  52. </div>
  53. <button
  54. onClick={() => markRead(first.id)}
  55. className="ml-auto flex-shrink-0 px-2 py-1 rounded font-medium text-bambu-gray-light hover:text-white hover:bg-bambu-dark-tertiary transition-colors"
  56. >
  57. {t('announcements.gotIt')}
  58. </button>
  59. </div>
  60. );
  61. }