feat: Implement Novu Inbox component

This commit is contained in:
gpt-engineer-app[bot]
2025-10-01 12:34:02 +00:00
parent 27b0a3ffff
commit 3436e317b5
5 changed files with 588 additions and 127 deletions

View File

@@ -1,55 +1,15 @@
import { useEffect, useState } from 'react';
import { useAuth } from '@/hooks/useAuth';
export interface NotificationItem {
id: string;
content: string;
read: boolean;
createdAt: string;
cta?: {
type: string;
data: any;
};
}
export function useNovuNotifications() {
const { user } = useAuth();
const [notifications, setNotifications] = useState<NotificationItem[]>([]);
const [unreadCount, setUnreadCount] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const applicationIdentifier = import.meta.env.VITE_NOVU_APPLICATION_IDENTIFIER;
const isEnabled = !!applicationIdentifier && !!user;
useEffect(() => {
if (!isEnabled) {
setIsLoading(false);
return;
}
// TODO: Initialize Novu Headless SDK when configuration is complete
// This will require the @novu/headless package to be properly configured
setIsLoading(false);
}, [isEnabled, user]);
const markAsRead = async (notificationId: string) => {
setNotifications((prev) =>
prev.map((n) => (n.id === notificationId ? { ...n, read: true } : n))
);
setUnreadCount((prev) => Math.max(0, prev - 1));
};
const markAllAsRead = async () => {
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
setUnreadCount(0);
};
const subscriberId = user?.id;
const isEnabled = !!applicationIdentifier && !!subscriberId;
return {
notifications,
unreadCount,
isLoading,
markAsRead,
markAllAsRead,
applicationIdentifier,
subscriberId,
isEnabled,
};
}

131
src/hooks/useNovuTheme.ts Normal file
View File

@@ -0,0 +1,131 @@
import { useTheme } from '@/components/theme/ThemeProvider';
import { useMemo } from 'react';
export function useNovuTheme() {
const { theme } = useTheme();
const appearance = useMemo(() => {
// Get computed styles to access CSS variables
const root = document.documentElement;
const style = getComputedStyle(root);
return {
variables: {
// Colors
colorBackground: `hsl(var(--background))`,
colorForeground: `hsl(var(--foreground))`,
colorPrimary: `hsl(var(--primary))`,
colorPrimaryForeground: `hsl(var(--primary-foreground))`,
colorSecondary: `hsl(var(--secondary))`,
colorSecondaryForeground: `hsl(var(--secondary-foreground))`,
colorCounter: `hsl(var(--primary))`,
colorCounterForeground: `hsl(var(--primary-foreground))`,
// Notification item colors
colorNeutral: `hsl(var(--muted))`,
colorNeutralForeground: `hsl(var(--muted-foreground))`,
// Border and divider
colorBorder: `hsl(var(--border))`,
// Border radius
borderRadius: `var(--radius)`,
// Font
fontFamily: style.getPropertyValue('font-family') || 'inherit',
fontSize: '14px',
},
elements: {
bellContainer: {
width: '40px',
height: '40px',
},
bell: {
width: '20px',
height: '20px',
color: `hsl(var(--foreground))`,
},
bellDot: {
backgroundColor: `hsl(var(--primary))`,
},
popover: {
boxShadow: `var(--shadow-card)`,
border: `1px solid hsl(var(--border))`,
borderRadius: `calc(var(--radius) + 4px)`,
},
notificationItem: {
transition: 'var(--transition-smooth)',
'&:hover': {
backgroundColor: `hsl(var(--muted) / 0.5)`,
},
},
notificationItemRead: {
opacity: '0.7',
},
notificationItemUnread: {
backgroundColor: `hsl(var(--muted) / 0.3)`,
borderLeft: `3px solid hsl(var(--primary))`,
},
notificationDot: {
backgroundColor: `hsl(var(--primary))`,
width: '8px',
height: '8px',
},
notificationTitle: {
fontWeight: '500',
color: `hsl(var(--foreground))`,
},
notificationDescription: {
color: `hsl(var(--muted-foreground))`,
},
notificationTimestamp: {
fontSize: '12px',
color: `hsl(var(--muted-foreground))`,
},
notificationPrimaryAction: {
backgroundColor: `hsl(var(--primary))`,
color: `hsl(var(--primary-foreground))`,
borderRadius: `var(--radius)`,
padding: '8px 16px',
transition: 'var(--transition-smooth)',
'&:hover': {
opacity: '0.9',
},
},
notificationSecondaryAction: {
backgroundColor: `hsl(var(--secondary))`,
color: `hsl(var(--secondary-foreground))`,
borderRadius: `var(--radius)`,
padding: '8px 16px',
transition: 'var(--transition-smooth)',
'&:hover': {
backgroundColor: `hsl(var(--secondary) / 0.8)`,
},
},
loader: {
color: `hsl(var(--primary))`,
},
emptyNotifications: {
color: `hsl(var(--muted-foreground))`,
textAlign: 'center',
padding: '32px 16px',
},
header: {
borderBottom: `1px solid hsl(var(--border))`,
padding: '16px',
},
headerTitle: {
fontSize: '16px',
fontWeight: '600',
color: `hsl(var(--foreground))`,
},
footer: {
borderTop: `1px solid hsl(var(--border))`,
padding: '12px 16px',
},
},
};
}, [theme]);
return appearance;
}