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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user