Phase 1: Progress modal text, SiteSerializer fields, Notification store, SiteCard checklist
- Improved progress modal messages in ai/engine.py (Section 4) - Added keywords_count and has_integration to SiteSerializer (Section 6) - Added notificationStore.ts for frontend notifications (Section 8) - Added NotificationDropdownNew component (Section 8) - Added SiteSetupChecklist to SiteCard in compact mode (Section 6) - Updated api.ts Site interface with new fields
This commit is contained in:
205
frontend/src/store/notificationStore.ts
Normal file
205
frontend/src/store/notificationStore.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Notification Store
|
||||
* Manages notifications for AI task completions and system events
|
||||
*
|
||||
* Features:
|
||||
* - In-memory notification queue
|
||||
* - Auto-dismissal with configurable timeout
|
||||
* - Read/unread state tracking
|
||||
* - Category-based filtering (ai_task, system, info)
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
export type NotificationType = 'success' | 'error' | 'warning' | 'info';
|
||||
export type NotificationCategory = 'ai_task' | 'system' | 'info';
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
type: NotificationType;
|
||||
category: NotificationCategory;
|
||||
title: string;
|
||||
message: string;
|
||||
timestamp: Date;
|
||||
read: boolean;
|
||||
actionLabel?: string;
|
||||
actionHref?: string;
|
||||
metadata?: {
|
||||
taskId?: string;
|
||||
functionName?: string;
|
||||
count?: number;
|
||||
credits?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface NotificationStore {
|
||||
notifications: Notification[];
|
||||
unreadCount: number;
|
||||
|
||||
// Actions
|
||||
addNotification: (notification: Omit<Notification, 'id' | 'timestamp' | 'read'>) => void;
|
||||
markAsRead: (id: string) => void;
|
||||
markAllAsRead: () => void;
|
||||
removeNotification: (id: string) => void;
|
||||
clearAll: () => void;
|
||||
|
||||
// AI Task specific
|
||||
addAITaskNotification: (
|
||||
functionName: string,
|
||||
success: boolean,
|
||||
message: string,
|
||||
metadata?: Notification['metadata']
|
||||
) => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STORE IMPLEMENTATION
|
||||
// ============================================================================
|
||||
|
||||
const generateId = () => `notif_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
||||
|
||||
export const useNotificationStore = create<NotificationStore>((set, get) => ({
|
||||
notifications: [],
|
||||
unreadCount: 0,
|
||||
|
||||
addNotification: (notification) => {
|
||||
const newNotification: Notification = {
|
||||
...notification,
|
||||
id: generateId(),
|
||||
timestamp: new Date(),
|
||||
read: false,
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
notifications: [newNotification, ...state.notifications].slice(0, 50), // Keep last 50
|
||||
unreadCount: state.unreadCount + 1,
|
||||
}));
|
||||
},
|
||||
|
||||
markAsRead: (id) => {
|
||||
set((state) => ({
|
||||
notifications: state.notifications.map((n) =>
|
||||
n.id === id ? { ...n, read: true } : n
|
||||
),
|
||||
unreadCount: Math.max(0, state.notifications.filter(n => !n.read && n.id !== id).length),
|
||||
}));
|
||||
},
|
||||
|
||||
markAllAsRead: () => {
|
||||
set((state) => ({
|
||||
notifications: state.notifications.map((n) => ({ ...n, read: true })),
|
||||
unreadCount: 0,
|
||||
}));
|
||||
},
|
||||
|
||||
removeNotification: (id) => {
|
||||
set((state) => {
|
||||
const notification = state.notifications.find(n => n.id === id);
|
||||
const wasUnread = notification && !notification.read;
|
||||
return {
|
||||
notifications: state.notifications.filter((n) => n.id !== id),
|
||||
unreadCount: wasUnread ? Math.max(0, state.unreadCount - 1) : state.unreadCount,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
clearAll: () => {
|
||||
set({ notifications: [], unreadCount: 0 });
|
||||
},
|
||||
|
||||
addAITaskNotification: (functionName, success, message, metadata) => {
|
||||
const displayNames: Record<string, string> = {
|
||||
'auto_cluster': 'Keyword Clustering',
|
||||
'generate_ideas': 'Idea Generation',
|
||||
'generate_content': 'Content Generation',
|
||||
'generate_images': 'Image Generation',
|
||||
'generate_image_prompts': 'Image Prompts',
|
||||
'optimize_content': 'Content Optimization',
|
||||
};
|
||||
|
||||
const actionHrefs: Record<string, string> = {
|
||||
'auto_cluster': '/planner/clusters',
|
||||
'generate_ideas': '/planner/ideas',
|
||||
'generate_content': '/writer/content',
|
||||
'generate_images': '/writer/images',
|
||||
'generate_image_prompts': '/writer/images',
|
||||
'optimize_content': '/writer/content',
|
||||
};
|
||||
|
||||
const title = displayNames[functionName] || functionName.replace(/_/g, ' ');
|
||||
|
||||
get().addNotification({
|
||||
type: success ? 'success' : 'error',
|
||||
category: 'ai_task',
|
||||
title: success ? `${title} Complete` : `${title} Failed`,
|
||||
message,
|
||||
actionLabel: success ? 'View Results' : 'Retry',
|
||||
actionHref: actionHrefs[functionName] || '/dashboard',
|
||||
metadata: {
|
||||
...metadata,
|
||||
functionName,
|
||||
},
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Format notification timestamp as relative time
|
||||
*/
|
||||
export function formatNotificationTime(timestamp: Date): string {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - timestamp.getTime();
|
||||
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const days = Math.floor(diff / 86400000);
|
||||
|
||||
if (minutes < 1) return 'Just now';
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
if (days < 7) return `${days}d ago`;
|
||||
|
||||
return timestamp.toLocaleDateString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get icon color classes for notification type
|
||||
*/
|
||||
export function getNotificationColors(type: NotificationType): {
|
||||
bg: string;
|
||||
icon: string;
|
||||
border: string;
|
||||
} {
|
||||
const colors = {
|
||||
success: {
|
||||
bg: 'bg-green-50 dark:bg-green-900/20',
|
||||
icon: 'text-green-500',
|
||||
border: 'border-green-200 dark:border-green-800',
|
||||
},
|
||||
error: {
|
||||
bg: 'bg-red-50 dark:bg-red-900/20',
|
||||
icon: 'text-red-500',
|
||||
border: 'border-red-200 dark:border-red-800',
|
||||
},
|
||||
warning: {
|
||||
bg: 'bg-amber-50 dark:bg-amber-900/20',
|
||||
icon: 'text-amber-500',
|
||||
border: 'border-amber-200 dark:border-amber-800',
|
||||
},
|
||||
info: {
|
||||
bg: 'bg-blue-50 dark:bg-blue-900/20',
|
||||
icon: 'text-blue-500',
|
||||
border: 'border-blue-200 dark:border-blue-800',
|
||||
},
|
||||
};
|
||||
|
||||
return colors[type];
|
||||
}
|
||||
Reference in New Issue
Block a user