Implement notification preferences (Task 16)

- Create NotificationSettings component with map-based area selector
- Add radius configuration slider (1-50km range)
- Implement notification type toggles (email, in-app)
- Add notifications tab to user profile page (visible only to profile owner)
- Update backend to parse and return areasOfInterest JSON
- Save and load user notification preferences
- Requirements: 10.1, 10.4
This commit is contained in:
2025-11-16 22:01:01 +00:00
parent 515e85b1eb
commit 86169f6141
7 changed files with 366 additions and 6 deletions

View File

@@ -4,6 +4,7 @@ import { useAuth } from '@/hooks/useAuth';
import { userService, type UserProfile, type UserActivity } from '@/services/userService';
import type { Report, CleanupEvent } from '@/types';
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet';
import { NotificationSettings, type NotificationPreferences } from '@/components/auth';
import 'leaflet/dist/leaflet.css';
import L from 'leaflet';
@@ -30,13 +31,18 @@ export function UserProfilePage() {
const [activity, setActivity] = useState<UserActivity[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'overview' | 'reports' | 'events' | 'activity'>('overview');
const [activeTab, setActiveTab] = useState<'overview' | 'reports' | 'events' | 'activity' | 'notifications'>('overview');
const [isEditing, setIsEditing] = useState(false);
const [editForm, setEditForm] = useState({
firstName: '',
lastName: '',
username: '',
});
const [notificationPreferences, setNotificationPreferences] = useState<NotificationPreferences>({
notificationEmail: true,
notificationInApp: true,
areasOfInterest: [],
});
const isOwnProfile = currentUser?.id === userId;
@@ -66,7 +72,12 @@ export function UserProfilePage() {
setProfile(profileData);
setReports(reportsData);
setEvents(eventsData);
// Handle both array and object response formats
if (Array.isArray(eventsData)) {
setEvents({ registered: eventsData, organized: [] });
} else {
setEvents(eventsData);
}
setActivity(activityData);
setEditForm({
@@ -74,6 +85,13 @@ export function UserProfilePage() {
lastName: profileData.lastName || '',
username: profileData.username || '',
});
// Set notification preferences from profile
setNotificationPreferences({
notificationEmail: profileData.notificationEmail ?? true,
notificationInApp: profileData.notificationInApp ?? true,
areasOfInterest: profileData.areasOfInterest || [],
});
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load profile');
} finally {
@@ -108,6 +126,30 @@ export function UserProfilePage() {
}
};
const handleSaveNotificationPreferences = async (preferences: NotificationPreferences) => {
if (!userId) return;
try {
await userService.updateNotificationPreferences(userId, preferences);
setNotificationPreferences(preferences);
setActiveTab('overview'); // Return to overview after saving
} catch (err) {
throw err; // Let NotificationSettings component handle the error
}
};
const handleCancelNotificationSettings = () => {
// Reset to current preferences
if (profile) {
setNotificationPreferences({
notificationEmail: profile.notificationEmail ?? true,
notificationInApp: profile.notificationInApp ?? true,
areasOfInterest: profile.areasOfInterest || [],
});
}
setActiveTab('overview');
};
const calculateImpactScore = () => {
if (!profile) return 0;
// Impact score calculation: reports * 10 + events attended * 20
@@ -254,7 +296,7 @@ export function UserProfilePage() {
<div className="bg-white rounded-lg shadow-md mb-6">
<div className="border-b border-gray-200">
<nav className="flex -mb-px">
{(['overview', 'reports', 'events', 'activity'] as const).map((tab) => (
{(['overview', 'reports', 'events', 'activity', ...(isOwnProfile ? ['notifications'] as const : [])] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
@@ -503,6 +545,14 @@ export function UserProfilePage() {
)}
</div>
)}
{activeTab === 'notifications' && isOwnProfile && (
<NotificationSettings
preferences={notificationPreferences}
onSave={handleSaveNotificationPreferences}
onCancel={handleCancelNotificationSettings}
/>
)}
</div>
</div>
</div>