Phase 2: Tags UI, Custom Fields UI, Notifications Bell
- Tags UI: TagsPage (CRUD, color picker), TagBadge, TagSelector (multi-select, inline creation) - Custom Fields Backend: model, schema, service, routes, migration 0041 - Custom Fields Frontend: CustomFieldsPage (definitions CRUD), CustomFieldRenderer (dynamic field rendering) - Custom Fields: _collect_custom_field_definitions() extended to merge DB definitions with plugin definitions - Notifications Bell: NotificationBell (30s polling, unread badge), NotificationDropdown, NotificationItem - NotificationBell integrated into TopBar - Routes: /tags, /settings/custom-fields registered - Settings nav: Custom Fields entry added - Menu items: Tags added to automation plugin manifest
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* NotificationBell — bell icon with unread badge + dropdown.
|
||||
*
|
||||
* - Uses useUnreadNotificationCount() with 30 s polling
|
||||
* - Bell icon (lucide-react Bell) with red badge count if > 0
|
||||
* - Click toggles dropdown
|
||||
* - Click outside closes dropdown
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Bell } from 'lucide-react';
|
||||
import { useUnreadNotificationCount } from '@/api/notifications';
|
||||
import { NotificationDropdown } from '@/components/notifications/NotificationDropdown';
|
||||
|
||||
export function NotificationBell() {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data: unreadCount } = useUnreadNotificationCount({
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
|
||||
// Close on click outside
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [open]);
|
||||
|
||||
// Close on Escape
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setOpen(false);
|
||||
};
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
return () => document.removeEventListener('keydown', handleEscape);
|
||||
}, [open]);
|
||||
|
||||
const count = unreadCount ?? 0;
|
||||
const displayCount = count > 99 ? '99+' : String(count);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="relative p-2 rounded-md hover:bg-secondary-100 text-secondary-600 min-h-touch min-w-touch flex items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
||||
aria-label={t('notifications.title', 'Benachrichtigungen')}
|
||||
aria-expanded={open}
|
||||
aria-haspopup="menu"
|
||||
>
|
||||
<Bell className="w-5 h-5" strokeWidth={2} aria-hidden="true" />
|
||||
|
||||
{count > 0 && (
|
||||
<span
|
||||
className="absolute -top-0.5 -right-0.5 flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-danger-500 text-white text-[10px] font-bold leading-none"
|
||||
aria-label={`${count} ungelesene Benachrichtigungen`}
|
||||
>
|
||||
{displayCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{open && <NotificationDropdown />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { Avatar } from '@/components/ui/Avatar';
|
||||
import { SearchDropdown } from '@/components/shared/SearchDropdown';
|
||||
import { SuggestionBadge } from '@/components/ai/SuggestionBadge';
|
||||
import { Building, ChevronDown, Menu, Zap, Bot, Layers } from 'lucide-react';
|
||||
import { NotificationBell } from '@/components/layout/NotificationBell';
|
||||
import { useWindowStore } from '@/store/windowStore';
|
||||
|
||||
export function TopBar() {
|
||||
@@ -80,6 +81,7 @@ export function TopBar() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<NotificationBell />
|
||||
{/* Minimized windows */}
|
||||
{minimizedWindows.length > 0 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
|
||||
Reference in New Issue
Block a user