localgreenchain/components/notifications/PreferencesForm.tsx
Claude 62c1ded598
Add comprehensive notification system (Agent 8)
Implement multi-channel notification system with:
- Core notification service with email, push, and in-app channels
- Email templates for all notification types (welcome, plant registered,
  transport alerts, farm alerts, harvest ready, demand matches, weekly digest)
- Push notification support with VAPID authentication
- In-app notification management with read/unread tracking
- Notification scheduler for recurring and scheduled notifications
- API endpoints for notifications CRUD, preferences, and subscriptions
- UI components (NotificationBell, NotificationList, NotificationItem,
  PreferencesForm)
- Full notifications page with preferences management
- Service worker for push notification handling
2025-11-23 03:52:41 +00:00

285 lines
9.7 KiB
TypeScript

/**
* PreferencesForm Component
* User notification preferences management
*/
import React, { useState, useEffect } from 'react';
interface NotificationPreferences {
email: boolean;
push: boolean;
inApp: boolean;
plantReminders: boolean;
transportAlerts: boolean;
farmAlerts: boolean;
harvestAlerts: boolean;
demandMatches: boolean;
weeklyDigest: boolean;
quietHoursStart?: string;
quietHoursEnd?: string;
timezone?: string;
}
interface PreferencesFormProps {
userId?: string;
onSave?: (preferences: NotificationPreferences) => void;
}
export function PreferencesForm({ userId = 'demo-user', onSave }: PreferencesFormProps) {
const [preferences, setPreferences] = useState<NotificationPreferences>({
email: true,
push: true,
inApp: true,
plantReminders: true,
transportAlerts: true,
farmAlerts: true,
harvestAlerts: true,
demandMatches: true,
weeklyDigest: true
});
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
useEffect(() => {
fetchPreferences();
}, [userId]);
async function fetchPreferences() {
try {
const response = await fetch(`/api/notifications/preferences?userId=${userId}`);
const data = await response.json();
if (data.success) {
setPreferences(data.data);
}
} catch (error) {
console.error('Failed to fetch preferences:', error);
} finally {
setIsLoading(false);
}
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setIsSaving(true);
setMessage(null);
try {
const response = await fetch('/api/notifications/preferences', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...preferences, userId })
});
const data = await response.json();
if (data.success) {
setMessage({ type: 'success', text: 'Preferences saved successfully!' });
onSave?.(data.data);
} else {
setMessage({ type: 'error', text: data.error || 'Failed to save preferences' });
}
} catch (error) {
setMessage({ type: 'error', text: 'Failed to save preferences' });
} finally {
setIsSaving(false);
}
}
function handleToggle(key: keyof NotificationPreferences) {
setPreferences(prev => ({
...prev,
[key]: !prev[key]
}));
}
if (isLoading) {
return (
<div className="p-8 text-center">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-4 border-gray-200 border-t-green-500"></div>
<p className="mt-2 text-gray-500">Loading preferences...</p>
</div>
);
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
{message && (
<div
className={`p-4 rounded-lg ${
message.type === 'success' ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800'
}`}
>
{message.text}
</div>
)}
{/* Notification Channels */}
<div className="bg-white p-6 rounded-lg border">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Notification Channels</h3>
<p className="text-sm text-gray-600 mb-4">Choose how you want to receive notifications</p>
<div className="space-y-4">
<ToggleRow
label="Email notifications"
description="Receive notifications via email"
enabled={preferences.email}
onChange={() => handleToggle('email')}
/>
<ToggleRow
label="Push notifications"
description="Receive browser push notifications"
enabled={preferences.push}
onChange={() => handleToggle('push')}
/>
<ToggleRow
label="In-app notifications"
description="See notifications in the app"
enabled={preferences.inApp}
onChange={() => handleToggle('inApp')}
/>
</div>
</div>
{/* Notification Types */}
<div className="bg-white p-6 rounded-lg border">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Notification Types</h3>
<p className="text-sm text-gray-600 mb-4">Choose which types of notifications you want to receive</p>
<div className="space-y-4">
<ToggleRow
label="Plant reminders"
description="Reminders for watering, fertilizing, and plant care"
enabled={preferences.plantReminders}
onChange={() => handleToggle('plantReminders')}
/>
<ToggleRow
label="Transport alerts"
description="Updates about plant transport and logistics"
enabled={preferences.transportAlerts}
onChange={() => handleToggle('transportAlerts')}
/>
<ToggleRow
label="Farm alerts"
description="Alerts about vertical farm conditions and issues"
enabled={preferences.farmAlerts}
onChange={() => handleToggle('farmAlerts')}
/>
<ToggleRow
label="Harvest alerts"
description="Notifications when crops are ready for harvest"
enabled={preferences.harvestAlerts}
onChange={() => handleToggle('harvestAlerts')}
/>
<ToggleRow
label="Demand matches"
description="Alerts when your supply matches consumer demand"
enabled={preferences.demandMatches}
onChange={() => handleToggle('demandMatches')}
/>
<ToggleRow
label="Weekly digest"
description="Weekly summary of your activity and insights"
enabled={preferences.weeklyDigest}
onChange={() => handleToggle('weeklyDigest')}
/>
</div>
</div>
{/* Quiet Hours */}
<div className="bg-white p-6 rounded-lg border">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Quiet Hours</h3>
<p className="text-sm text-gray-600 mb-4">Set times when you don't want to receive notifications</p>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Start time</label>
<input
type="time"
value={preferences.quietHoursStart || ''}
onChange={e =>
setPreferences(prev => ({ ...prev, quietHoursStart: e.target.value }))
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">End time</label>
<input
type="time"
value={preferences.quietHoursEnd || ''}
onChange={e =>
setPreferences(prev => ({ ...prev, quietHoursEnd: e.target.value }))
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500"
/>
</div>
</div>
<div className="mt-4">
<label className="block text-sm font-medium text-gray-700 mb-1">Timezone</label>
<select
value={preferences.timezone || ''}
onChange={e => setPreferences(prev => ({ ...prev, timezone: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500"
>
<option value="">Select timezone</option>
<option value="America/New_York">Eastern Time</option>
<option value="America/Chicago">Central Time</option>
<option value="America/Denver">Mountain Time</option>
<option value="America/Los_Angeles">Pacific Time</option>
<option value="Europe/London">London</option>
<option value="Europe/Paris">Paris</option>
<option value="Asia/Tokyo">Tokyo</option>
<option value="UTC">UTC</option>
</select>
</div>
</div>
{/* Submit */}
<div className="flex justify-end">
<button
type="submit"
disabled={isSaving}
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 focus:ring-2 focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isSaving ? 'Saving...' : 'Save Preferences'}
</button>
</div>
</form>
);
}
interface ToggleRowProps {
label: string;
description: string;
enabled: boolean;
onChange: () => void;
}
function ToggleRow({ label, description, enabled, onChange }: ToggleRowProps) {
return (
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">{label}</p>
<p className="text-sm text-gray-500">{description}</p>
</div>
<button
type="button"
role="switch"
aria-checked={enabled}
onClick={onChange}
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
enabled ? 'bg-green-600' : 'bg-gray-200'
}`}
>
<span
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
enabled ? 'translate-x-5' : 'translate-x-0'
}`}
/>
</button>
</div>
);
}