Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions NOTIFICATION_SYSTEM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# User Notification System

## Overview
A toast-style notification system has been added to the Digital Humanities Dashboard to provide real-time feedback to users.

## Features

### 1. **Multiple Notification Types**
- **Info** (blue): General information messages
- **Success** (green): Successful actions or confirmations
- **Warning** (amber/yellow): Important warnings or alerts
- **Error** (red): Error messages

### 2. **Auto-dismiss**
- Notifications automatically disappear after a configurable duration (default: 5 seconds)
- Can be set to 0 for persistent notifications

### 3. **Manual Dismiss**
- Users can close notifications manually by clicking the X button

### 4. **Smooth Animations**
- Slide-in animation when notifications appear
- Slide-out animation when dismissing
- Positioned in top-right corner for non-intrusive user experience

## Implementation Details

### Components Added:
1. **NotificationContext**: React Context for managing notification state
2. **NotificationProvider**: Provider component that wraps the app
3. **NotificationContainer**: Container for rendering all active notifications
4. **Notification**: Individual notification component with styling and animations

### Usage in Dashboard:
- **Welcome message**: Shows when the dashboard first loads
- **Tab change notifications**: Provides feedback when switching between analysis views

## How to Use

To add a notification anywhere in the app, use the `addNotification` function from the context:

```javascript
const { addNotification } = useNotification();

// Examples:
addNotification('Your message here', 'success', 3000);
addNotification('Warning message', 'warning', 5000);
addNotification('Error occurred', 'error', 4000);
addNotification('Information', 'info', 3000);
```

### Parameters:
- `message` (string): The notification text
- `type` (string): One of 'info', 'success', 'warning', or 'error' (default: 'info')
- `duration` (number): Time in milliseconds before auto-dismiss (default: 5000, set to 0 for persistent)

## Visual Design
- Uses Tailwind CSS for styling
- Integrates FontAwesome icons for visual indicators
- Matches the existing dashboard design language
- Responsive and works on all screen sizes
150 changes: 145 additions & 5 deletions dh_car.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,144 @@
body { background-color: #f8fafc; }
.animate-fade-in { animation: fadeIn 0.5s ease-in; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.animate-slide-in { animation: slideIn 0.3s ease-out; }
@keyframes slideIn { from { opacity: 0; transform: translateX(100%); } to { opacity: 1; transform: translateX(0); } }
.animate-slide-out { animation: slideOut 0.3s ease-in; }
@keyframes slideOut { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(100%); } }
</style>
</head>
<body>
<div id="root"></div>

<script type="text/babel">
const { useState } = React;
const { useState, useEffect, createContext, useContext, useCallback, useRef } = React;

// Safe destructuring of Recharts components
const {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer,
LineChart, Line, AreaChart, Area
} = window.Recharts;

// --- NOTIFICATION SYSTEM ---

const SLIDE_OUT_DURATION = 300; // milliseconds - matches CSS animation
const NotificationContext = createContext();

const useNotification = () => {
const context = useContext(NotificationContext);
if (!context) {
throw new Error('useNotification must be used within NotificationProvider');
}
return context;
};

const NotificationProvider = ({ children }) => {
const [notifications, setNotifications] = useState([]);
const counterRef = useRef(0);
const timeoutsRef = useRef({});
const removeNotificationRef = useRef();

const removeNotification = useCallback((id) => {
setNotifications(prev => prev.filter(notif => notif.id !== id));
if (timeoutsRef.current[id]) {
clearTimeout(timeoutsRef.current[id]);
delete timeoutsRef.current[id];
}
}, []);

// Keep ref updated to avoid stale closures in setTimeout
removeNotificationRef.current = removeNotification;

const addNotification = useCallback((message, type = 'info', duration = 5000) => {
const id = counterRef.current;
counterRef.current += 1;
setNotifications(prev => [...prev, { id, message, type, duration }]);

if (duration > 0) {
timeoutsRef.current[id] = setTimeout(() => {
removeNotificationRef.current(id);
}, duration);
}
}, []);

// Cleanup all timeouts on unmount
useEffect(() => {
return () => {
Object.values(timeoutsRef.current).forEach(clearTimeout);
};
}, []);

return (
<NotificationContext.Provider value={{ addNotification, removeNotification }}>
{children}
<NotificationContainer notifications={notifications} removeNotification={removeNotification} />
</NotificationContext.Provider>
);
};

const NotificationContainer = ({ notifications, removeNotification }) => {
return (
<div className="fixed top-4 right-4 z-50 space-y-2 max-w-md">
{notifications.map(notif => (
<Notification key={notif.id} {...notif} onClose={() => removeNotification(notif.id)} />
))}
</div>
);
};

// Notification style constants (moved outside to avoid recreation on each render)
const NOTIFICATION_TYPE_STYLES = {
info: 'bg-blue-50 border-blue-200 text-blue-800',
success: 'bg-green-50 border-green-200 text-green-800',
warning: 'bg-amber-50 border-amber-200 text-amber-800',
error: 'bg-red-50 border-red-200 text-red-800'
};

const NOTIFICATION_ICON_STYLES = {
info: 'fa-circle-info text-blue-500',
success: 'fa-circle-check text-green-500',
warning: 'fa-triangle-exclamation text-amber-500',
error: 'fa-circle-xmark text-red-500'
};

const Notification = ({ message, type, onClose }) => {
const [isLeaving, setIsLeaving] = useState(false);
const closeTimeoutRef = useRef(null);

useEffect(() => {
return () => {
if (closeTimeoutRef.current) {
clearTimeout(closeTimeoutRef.current);
}
};
}, []);

const handleClose = () => {
setIsLeaving(true);
closeTimeoutRef.current = setTimeout(onClose, SLIDE_OUT_DURATION);
};

const animationClass = isLeaving ? 'animate-slide-out' : 'animate-slide-in';
const colorClass = NOTIFICATION_TYPE_STYLES[type] || NOTIFICATION_TYPE_STYLES.info;
const notificationClasses = `${colorClass} ${animationClass} border rounded-lg shadow-lg p-4 flex items-start gap-3 min-w-[300px]`;
const iconClasses = `fa-solid ${NOTIFICATION_ICON_STYLES[type] || NOTIFICATION_ICON_STYLES.info} text-xl mt-0.5`;

return (
<div className={notificationClasses}>
<i className={iconClasses}></i>
<div className="flex-1">
<p className="text-sm font-medium">{message}</p>
</div>
<button
onClick={handleClose}
className="text-slate-400 hover:text-slate-600 transition-colors"
>
<i className="fa-solid fa-xmark"></i>
</button>
</div>
);
};

// --- DATASETS ---

const genderData = [
Expand Down Expand Up @@ -71,6 +195,18 @@

const Dashboard = () => {
const [activeTab, setActiveTab] = useState('gender');
const { addNotification } = useNotification();

// Show a welcome notification on mount
useEffect(() => {
addNotification('Welcome to the Digital Humanities Dashboard!', 'success', 4000);
}, [addNotification]);

// Helper function to show notifications on tab change
const handleTabChange = useCallback((tab, message) => {
setActiveTab(tab);
addNotification(message, 'info', 3000);
}, [addNotification]);

return (
<div className="min-h-screen bg-slate-50 font-sans text-slate-800 p-6 max-w-7xl mx-auto">
Expand All @@ -88,21 +224,21 @@ <h2 className="text-xl text-slate-500">The Literary Landscape of C.A.R. (1980s
{/* NAVIGATION */}
<div className="flex flex-wrap gap-4 mb-8">
<button
onClick={() => setActiveTab('gender')}
onClick={() => handleTabChange('gender', 'Viewing Gender Representation analysis')}
className={`px-4 py-2 rounded-lg flex items-center gap-2 font-medium transition-colors ${activeTab === 'gender' ? 'bg-indigo-600 text-white' : 'bg-white border border-slate-200 hover:bg-slate-50'}`}
>
<i className="fa-solid fa-users"></i>
Gender Representation
</button>
<button
onClick={() => setActiveTab('timeline')}
onClick={() => handleTabChange('timeline', 'Viewing Chronology of Flux')}
className={`px-4 py-2 rounded-lg flex items-center gap-2 font-medium transition-colors ${activeTab === 'timeline' ? 'bg-indigo-600 text-white' : 'bg-white border border-slate-200 hover:bg-slate-50'}`}
>
<i className="fa-solid fa-arrow-trend-up"></i>
Chronology of Flux
</button>
<button
onClick={() => setActiveTab('barriers')}
onClick={() => handleTabChange('barriers', 'Viewing Structural Barriers analysis')}
className={`px-4 py-2 rounded-lg flex items-center gap-2 font-medium transition-colors ${activeTab === 'barriers' ? 'bg-indigo-600 text-white' : 'bg-white border border-slate-200 hover:bg-slate-50'}`}
>
<i className="fa-solid fa-triangle-exclamation"></i>
Expand Down Expand Up @@ -258,7 +394,11 @@ <h4 className="font-bold text-green-800 mb-4 text-lg">The Solution: Cultural Sov
};

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Dashboard />);
root.render(
<NotificationProvider>
<Dashboard />
</NotificationProvider>
);
</script>
</body>
</html>