- Created useDirectoryConfig hook (src/hooks/use-directory-config.js)
- Updated Profile.js - conditional rendering with isFieldEnabled() - Updated MemberCard.js - conditional rendering for directory fields - Updated MembersDirectory.js - conditional rendering in profile dialog - Created AdminDirectorySettings.js - Admin UI for toggling fields - Updated SettingsSidebar.js - Added Directory and Registration tabs - Updated App.js - Added routes for new settings pages
This commit is contained in:
@@ -48,6 +48,7 @@ import AdminNewsletters from './pages/admin/AdminNewsletters';
|
|||||||
import AdminFinancials from './pages/admin/AdminFinancials';
|
import AdminFinancials from './pages/admin/AdminFinancials';
|
||||||
import AdminBylaws from './pages/admin/AdminBylaws';
|
import AdminBylaws from './pages/admin/AdminBylaws';
|
||||||
import AdminRegistrationBuilder from './pages/admin/AdminRegistrationBuilder';
|
import AdminRegistrationBuilder from './pages/admin/AdminRegistrationBuilder';
|
||||||
|
import AdminDirectorySettings from './pages/admin/AdminDirectorySettings';
|
||||||
import History from './pages/History';
|
import History from './pages/History';
|
||||||
import MissionValues from './pages/MissionValues';
|
import MissionValues from './pages/MissionValues';
|
||||||
import BoardOfDirectors from './pages/BoardOfDirectors';
|
import BoardOfDirectors from './pages/BoardOfDirectors';
|
||||||
@@ -319,6 +320,8 @@ function App() {
|
|||||||
<Route path="stripe" element={<AdminSettings />} />
|
<Route path="stripe" element={<AdminSettings />} />
|
||||||
<Route path="permissions" element={<AdminRoles />} />
|
<Route path="permissions" element={<AdminRoles />} />
|
||||||
<Route path="theme" element={<AdminTheme />} />
|
<Route path="theme" element={<AdminTheme />} />
|
||||||
|
<Route path="directory" element={<AdminDirectorySettings />} />
|
||||||
|
<Route path="registration" element={<AdminRegistrationBuilder />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* 404 - Catch all undefined routes */}
|
{/* 404 - Catch all undefined routes */}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import api from '../utils/api';
|
||||||
import logger from '../utils/logger';
|
import logger from '../utils/logger';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -20,6 +21,7 @@ import { AlertTriangle, RefreshCw } from 'lucide-react';
|
|||||||
* - Warns 1 minute before JWT expiry (at 29 minutes if JWT is 30 min)
|
* - Warns 1 minute before JWT expiry (at 29 minutes if JWT is 30 min)
|
||||||
* - Auto-logout on expiration
|
* - Auto-logout on expiration
|
||||||
* - "Stay Logged In" extends session
|
* - "Stay Logged In" extends session
|
||||||
|
* - Checks session validity when tab becomes visible after being hidden
|
||||||
*/
|
*/
|
||||||
const IdleSessionWarning = () => {
|
const IdleSessionWarning = () => {
|
||||||
const { user, logout, refreshUser } = useAuth();
|
const { user, logout, refreshUser } = useAuth();
|
||||||
@@ -33,11 +35,13 @@ const IdleSessionWarning = () => {
|
|||||||
const [showWarning, setShowWarning] = useState(false);
|
const [showWarning, setShowWarning] = useState(false);
|
||||||
const [timeRemaining, setTimeRemaining] = useState(60); // seconds
|
const [timeRemaining, setTimeRemaining] = useState(60); // seconds
|
||||||
const [isExtending, setIsExtending] = useState(false);
|
const [isExtending, setIsExtending] = useState(false);
|
||||||
|
const [isCheckingSession, setIsCheckingSession] = useState(false);
|
||||||
|
|
||||||
const activityTimeoutRef = useRef(null);
|
const activityTimeoutRef = useRef(null);
|
||||||
const warningTimeoutRef = useRef(null);
|
const warningTimeoutRef = useRef(null);
|
||||||
const countdownIntervalRef = useRef(null);
|
const countdownIntervalRef = useRef(null);
|
||||||
const lastActivityRef = useRef(Date.now());
|
const lastActivityRef = useRef(Date.now());
|
||||||
|
const lastVisibilityCheckRef = useRef(Date.now());
|
||||||
|
|
||||||
// Reset activity timer
|
// Reset activity timer
|
||||||
const resetActivityTimer = useCallback(() => {
|
const resetActivityTimer = useCallback(() => {
|
||||||
@@ -120,6 +124,83 @@ const IdleSessionWarning = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if session is still valid (called when tab becomes visible)
|
||||||
|
const checkSessionValidity = useCallback(async () => {
|
||||||
|
if (!user || isCheckingSession) return;
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) {
|
||||||
|
logger.log('[IdleSessionWarning] No token found on visibility change');
|
||||||
|
handleSessionExpired();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCheckingSession(true);
|
||||||
|
try {
|
||||||
|
// Make a lightweight API call to verify token is still valid
|
||||||
|
await api.get('/auth/me');
|
||||||
|
logger.log('[IdleSessionWarning] Session still valid after visibility change');
|
||||||
|
|
||||||
|
// Reset the activity timer since user is back
|
||||||
|
resetActivityTimer();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[IdleSessionWarning] Session invalid on visibility change:', error);
|
||||||
|
|
||||||
|
// If 401, the interceptor will handle redirect
|
||||||
|
// For other errors, still redirect to be safe
|
||||||
|
if (error.response?.status !== 401) {
|
||||||
|
handleSessionExpired();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsCheckingSession(false);
|
||||||
|
}
|
||||||
|
}, [user, isCheckingSession, handleSessionExpired, resetActivityTimer]);
|
||||||
|
|
||||||
|
// Handle visibility change (when user returns to tab after being away)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user) return;
|
||||||
|
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
const now = Date.now();
|
||||||
|
const timeSinceLastCheck = now - lastVisibilityCheckRef.current;
|
||||||
|
|
||||||
|
// Only check if tab was hidden for more than 1 minute
|
||||||
|
// This prevents unnecessary API calls for quick tab switches
|
||||||
|
if (timeSinceLastCheck > 60 * 1000) {
|
||||||
|
logger.log('[IdleSessionWarning] Tab became visible after', Math.round(timeSinceLastCheck / 1000), 'seconds');
|
||||||
|
checkSessionValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastVisibilityCheckRef.current = now;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
};
|
||||||
|
}, [user, checkSessionValidity]);
|
||||||
|
|
||||||
|
// Listen for session expired event from API interceptor
|
||||||
|
useEffect(() => {
|
||||||
|
const handleAuthSessionExpired = () => {
|
||||||
|
logger.log('[IdleSessionWarning] Received auth:session-expired event');
|
||||||
|
// Clear all timers
|
||||||
|
if (activityTimeoutRef.current) clearTimeout(activityTimeoutRef.current);
|
||||||
|
if (warningTimeoutRef.current) clearTimeout(warningTimeoutRef.current);
|
||||||
|
if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current);
|
||||||
|
setShowWarning(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('auth:session-expired', handleAuthSessionExpired);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('auth:session-expired', handleAuthSessionExpired);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Track user activity
|
// Track user activity
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Card } from './ui/card';
|
|||||||
import { Button } from './ui/button';
|
import { Button } from './ui/button';
|
||||||
import { Heart, Calendar, Mail, Phone, MapPin, Facebook, Instagram, Twitter, Linkedin, UserCircle } from 'lucide-react';
|
import { Heart, Calendar, Mail, Phone, MapPin, Facebook, Instagram, Twitter, Linkedin, UserCircle } from 'lucide-react';
|
||||||
import MemberBadge from './MemberBadge';
|
import MemberBadge from './MemberBadge';
|
||||||
|
import useDirectoryConfig from '../hooks/use-directory-config';
|
||||||
|
|
||||||
// Helper function to get initials
|
// Helper function to get initials
|
||||||
const getInitials = (firstName, lastName) => {
|
const getInitials = (firstName, lastName) => {
|
||||||
@@ -20,6 +21,7 @@ const getSocialMediaLink = (url) => {
|
|||||||
|
|
||||||
const MemberCard = ({ member, onViewProfile, tiers }) => {
|
const MemberCard = ({ member, onViewProfile, tiers }) => {
|
||||||
const memberSince = member.member_since || member.created_at;
|
const memberSince = member.member_since || member.created_at;
|
||||||
|
const { isFieldEnabled } = useDirectoryConfig();
|
||||||
return (
|
return (
|
||||||
<Card className="p-6 bg-background rounded-3xl border border-[var(--neutral-800)] hover:shadow-lg transition-all h-full">
|
<Card className="p-6 bg-background rounded-3xl border border-[var(--neutral-800)] hover:shadow-lg transition-all h-full">
|
||||||
{/* Member Tier Badge */}
|
{/* Member Tier Badge */}
|
||||||
@@ -48,7 +50,7 @@ const MemberCard = ({ member, onViewProfile, tiers }) => {
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* Partner Name */}
|
{/* Partner Name */}
|
||||||
{member.directory_partner_name && (
|
{isFieldEnabled('directory_partner_name') && member.directory_partner_name && (
|
||||||
<div className="flex items-center justify-center gap-2 mb-4">
|
<div className="flex items-center justify-center gap-2 mb-4">
|
||||||
<Heart className="h-4 w-4 text-[var(--orange-light)]" />
|
<Heart className="h-4 w-4 text-[var(--orange-light)]" />
|
||||||
<span className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<span className="text-sm text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
@@ -58,7 +60,7 @@ const MemberCard = ({ member, onViewProfile, tiers }) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Bio */}
|
{/* Bio */}
|
||||||
{member.directory_bio && (
|
{isFieldEnabled('directory_bio') && member.directory_bio && (
|
||||||
<p className="text-brand-purple text-center mb-4 line-clamp-3" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<p className="text-brand-purple text-center mb-4 line-clamp-3" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
{member.directory_bio}
|
{member.directory_bio}
|
||||||
</p>
|
</p>
|
||||||
@@ -79,7 +81,7 @@ const MemberCard = ({ member, onViewProfile, tiers }) => {
|
|||||||
|
|
||||||
{/* Contact Information */}
|
{/* Contact Information */}
|
||||||
<div className="space-y-3 mb-4">
|
<div className="space-y-3 mb-4">
|
||||||
{member.directory_email && (
|
{isFieldEnabled('directory_email') && member.directory_email && (
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<Mail className="h-4 w-4 text-brand-purple flex-shrink-0" />
|
<Mail className="h-4 w-4 text-brand-purple flex-shrink-0" />
|
||||||
<a
|
<a
|
||||||
@@ -92,7 +94,7 @@ const MemberCard = ({ member, onViewProfile, tiers }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{member.directory_phone && (
|
{isFieldEnabled('directory_phone') && member.directory_phone && (
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
<Phone className="h-4 w-4 text-brand-purple flex-shrink-0" />
|
<Phone className="h-4 w-4 text-brand-purple flex-shrink-0" />
|
||||||
<a
|
<a
|
||||||
@@ -105,7 +107,7 @@ const MemberCard = ({ member, onViewProfile, tiers }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{member.directory_address && (
|
{isFieldEnabled('directory_address') && member.directory_address && (
|
||||||
<div className="flex items-start gap-2 text-sm">
|
<div className="flex items-start gap-2 text-sm">
|
||||||
<MapPin className="h-4 w-4 text-brand-purple flex-shrink-0 mt-0.5" />
|
<MapPin className="h-4 w-4 text-brand-purple flex-shrink-0 mt-0.5" />
|
||||||
<span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<span className="text-brand-purple " style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
@@ -116,7 +118,7 @@ const MemberCard = ({ member, onViewProfile, tiers }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Social Media Links */}
|
{/* Social Media Links */}
|
||||||
{(member.social_media_facebook || member.social_media_instagram || member.social_media_twitter || member.social_media_linkedin) && (
|
{isFieldEnabled('social_media') && (member.social_media_facebook || member.social_media_instagram || member.social_media_twitter || member.social_media_linkedin) && (
|
||||||
<div className="pt-4 border-t border-[var(--neutral-800)]">
|
<div className="pt-4 border-t border-[var(--neutral-800)]">
|
||||||
<div className="flex justify-center gap-3">
|
<div className="flex justify-center gap-3">
|
||||||
{member.social_media_facebook && (
|
{member.social_media_facebook && (
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavLink, useLocation } from 'react-router-dom';
|
import { NavLink, useLocation } from 'react-router-dom';
|
||||||
import { CreditCard, Shield, Star, Palette, FileEdit } from 'lucide-react';
|
import { CreditCard, Shield, Star, Palette, FileEdit, BookUser } from 'lucide-react';
|
||||||
|
|
||||||
const settingsItems = [
|
const settingsItems = [
|
||||||
{ label: 'Stripe', path: '/admin/settings/stripe', icon: CreditCard },
|
{ label: 'Stripe', path: '/admin/settings/stripe', icon: CreditCard },
|
||||||
{ label: 'Permissions', path: '/admin/settings/permissions', icon: Shield },
|
{ label: 'Permissions', path: '/admin/settings/permissions', icon: Shield },
|
||||||
{ label: 'Theme', path: '/admin/settings/theme', icon: Palette },
|
{ label: 'Theme', path: '/admin/settings/theme', icon: Palette },
|
||||||
|
{ label: 'Directory', path: '/admin/settings/directory', icon: BookUser },
|
||||||
|
// { label: 'Registration', path: '/admin/settings/registration', icon: FileEdit }, // Commented out for future fallback
|
||||||
];
|
];
|
||||||
|
|
||||||
const SettingsTabs = () => {
|
const SettingsTabs = () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useNavigate, Link } from 'react-router-dom';
|
import { useNavigate, Link, useLocation, useSearchParams } from 'react-router-dom';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { Button } from '../components/ui/button';
|
import { Button } from '../components/ui/button';
|
||||||
import { Input } from '../components/ui/input';
|
import { Input } from '../components/ui/input';
|
||||||
@@ -13,6 +13,8 @@ import { ArrowRight, ArrowLeft } from 'lucide-react';
|
|||||||
|
|
||||||
const Login = () => {
|
const Login = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
@@ -20,6 +22,30 @@ const Login = () => {
|
|||||||
password: ''
|
password: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Show session expiry message on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const sessionParam = searchParams.get('session');
|
||||||
|
const stateMessage = location.state?.message;
|
||||||
|
|
||||||
|
if (sessionParam === 'expired') {
|
||||||
|
toast.info('Your session has expired. Please log in again.', {
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
// Clean up URL
|
||||||
|
window.history.replaceState({}, '', '/login');
|
||||||
|
} else if (sessionParam === 'idle') {
|
||||||
|
toast.info('You were logged out due to inactivity. Please log in again.', {
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
// Clean up URL
|
||||||
|
window.history.replaceState({}, '', '/login');
|
||||||
|
} else if (stateMessage) {
|
||||||
|
toast.info(stateMessage, {
|
||||||
|
duration: 5000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [searchParams, location.state]);
|
||||||
|
|
||||||
const handleInputChange = (e) => {
|
const handleInputChange = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { Avatar, AvatarImage, AvatarFallback } from '../components/ui/avatar';
|
|||||||
import ChangePasswordDialog from '../components/ChangePasswordDialog';
|
import ChangePasswordDialog from '../components/ChangePasswordDialog';
|
||||||
import PaymentMethodsSection from '../components/PaymentMethodsSection';
|
import PaymentMethodsSection from '../components/PaymentMethodsSection';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import useDirectoryConfig from '../hooks/use-directory-config';
|
||||||
|
|
||||||
const Profile = () => {
|
const Profile = () => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -29,6 +30,7 @@ const Profile = () => {
|
|||||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||||
const [initialFormData, setInitialFormData] = useState(null);
|
const [initialFormData, setInitialFormData] = useState(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { isFieldEnabled, loading: directoryConfigLoading } = useDirectoryConfig();
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
first_name: '',
|
first_name: '',
|
||||||
last_name: '',
|
last_name: '',
|
||||||
@@ -427,6 +429,7 @@ const Profile = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Member Directory Settings */}
|
{/* Member Directory Settings */}
|
||||||
|
{isFieldEnabled('show_in_directory') && (
|
||||||
<div className="pt-6 border-t border-[var(--neutral-800)] space-y-4">
|
<div className="pt-6 border-t border-[var(--neutral-800)] space-y-4">
|
||||||
<h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
<BookUser className="h-5 w-5 text-[var(--orange-light)]" />
|
<BookUser className="h-5 w-5 text-[var(--orange-light)]" />
|
||||||
@@ -452,6 +455,7 @@ const Profile = () => {
|
|||||||
|
|
||||||
{formData.show_in_directory && (
|
{formData.show_in_directory && (
|
||||||
<div className="space-y-4 pl-4 border-l-4 border-[var(--neutral-800)]">
|
<div className="space-y-4 pl-4 border-l-4 border-[var(--neutral-800)]">
|
||||||
|
{isFieldEnabled('directory_email') && (
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="directory_email">Directory Email</Label>
|
<Label htmlFor="directory_email">Directory Email</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -464,7 +468,9 @@ const Profile = () => {
|
|||||||
placeholder="Optional - email to show in directory"
|
placeholder="Optional - email to show in directory"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isFieldEnabled('directory_bio') && (
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="directory_bio">Bio</Label>
|
<Label htmlFor="directory_bio">Bio</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
@@ -476,7 +482,9 @@ const Profile = () => {
|
|||||||
placeholder="Tell other members about yourself..."
|
placeholder="Tell other members about yourself..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isFieldEnabled('directory_address') && (
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="directory_address">Address</Label>
|
<Label htmlFor="directory_address">Address</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -488,7 +496,9 @@ const Profile = () => {
|
|||||||
placeholder="Optional - address to show in directory"
|
placeholder="Optional - address to show in directory"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isFieldEnabled('directory_phone') && (
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="directory_phone">Phone</Label>
|
<Label htmlFor="directory_phone">Phone</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -501,7 +511,9 @@ const Profile = () => {
|
|||||||
placeholder="Optional - phone to show in directory"
|
placeholder="Optional - phone to show in directory"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isFieldEnabled('directory_dob') && (
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="directory_dob">Date of Birth</Label>
|
<Label htmlFor="directory_dob">Date of Birth</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -513,7 +525,9 @@ const Profile = () => {
|
|||||||
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
className="h-12 rounded-xl border-2 border-[var(--neutral-800)] focus:border-brand-purple"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isFieldEnabled('directory_partner_name') && (
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="directory_partner_name">Partner Name</Label>
|
<Label htmlFor="directory_partner_name">Partner Name</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -525,9 +539,11 @@ const Profile = () => {
|
|||||||
placeholder="Optional - partner name to show in directory"
|
placeholder="Optional - partner name to show in directory"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -664,6 +680,7 @@ const Profile = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Volunteer Interests */}
|
{/* Volunteer Interests */}
|
||||||
|
{isFieldEnabled('volunteer_interests') && (
|
||||||
<div className="pt-6 border-t border-[var(--neutral-800)] space-y-4">
|
<div className="pt-6 border-t border-[var(--neutral-800)] space-y-4">
|
||||||
<h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h4 className="text-lg font-semibold text-[var(--purple-ink)] flex items-center gap-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
<Users className="h-5 w-5 text-brand-purple" />
|
<Users className="h-5 w-5 text-brand-purple" />
|
||||||
@@ -692,6 +709,7 @@ const Profile = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import MemberCard from '../../components/MemberCard';
|
|||||||
import MemberBadge from '../../components/MemberBadge';
|
import MemberBadge from '../../components/MemberBadge';
|
||||||
import useMembers from '../../hooks/use-members';
|
import useMembers from '../../hooks/use-members';
|
||||||
import useMemberTiers from '../../hooks/use-member-tiers';
|
import useMemberTiers from '../../hooks/use-member-tiers';
|
||||||
|
import useDirectoryConfig from '../../hooks/use-directory-config';
|
||||||
|
|
||||||
const MembersDirectory = () => {
|
const MembersDirectory = () => {
|
||||||
const [selectedMember, setSelectedMember] = useState(null);
|
const [selectedMember, setSelectedMember] = useState(null);
|
||||||
@@ -28,6 +29,7 @@ const MembersDirectory = () => {
|
|||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const pageSize = 12;
|
const pageSize = 12;
|
||||||
const { tiers } = useMemberTiers();
|
const { tiers } = useMemberTiers();
|
||||||
|
const { isFieldEnabled } = useDirectoryConfig();
|
||||||
const allowedRoles = useMemo(() => [], []);
|
const allowedRoles = useMemo(() => [], []);
|
||||||
const normalizeStatus = useCallback((status) => {
|
const normalizeStatus = useCallback((status) => {
|
||||||
if (typeof status === 'string') {
|
if (typeof status === 'string') {
|
||||||
@@ -242,7 +244,7 @@ const MembersDirectory = () => {
|
|||||||
{selectedMember.first_name} {selectedMember.last_name}
|
{selectedMember.first_name} {selectedMember.last_name}
|
||||||
<MemberBadge memberSince={selectedMember.member_since || selectedMember.created_at} tiers={tiers} />
|
<MemberBadge memberSince={selectedMember.member_since || selectedMember.created_at} tiers={tiers} />
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
{selectedMember.directory_partner_name && (
|
{isFieldEnabled('directory_partner_name') && selectedMember.directory_partner_name && (
|
||||||
<DialogDescription className="flex items-center gap-2 text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
<DialogDescription className="flex items-center gap-2 text-lg" style={{ fontFamily: "'Nunito Sans', sans-serif" }}>
|
||||||
<Heart className="h-5 w-5 text-[var(--orange-light)]" />
|
<Heart className="h-5 w-5 text-[var(--orange-light)]" />
|
||||||
<span className="text-brand-purple ">Partner: {selectedMember.directory_partner_name}</span>
|
<span className="text-brand-purple ">Partner: {selectedMember.directory_partner_name}</span>
|
||||||
@@ -252,7 +254,7 @@ const MembersDirectory = () => {
|
|||||||
|
|
||||||
<div className="space-y-6 py-4">
|
<div className="space-y-6 py-4">
|
||||||
{/* Bio */}
|
{/* Bio */}
|
||||||
{selectedMember.directory_bio && (
|
{isFieldEnabled('directory_bio') && selectedMember.directory_bio && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-2" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
About
|
About
|
||||||
@@ -264,12 +266,13 @@ const MembersDirectory = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Contact Information */}
|
{/* Contact Information */}
|
||||||
|
{(isFieldEnabled('directory_email') || isFieldEnabled('directory_phone') || isFieldEnabled('directory_address') || isFieldEnabled('directory_dob')) && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
Contact Information
|
Contact Information
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{selectedMember.directory_email && (
|
{isFieldEnabled('directory_email') && selectedMember.directory_email && (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 rounded-lg bg-[var(--lavender-500)]">
|
<div className="p-2 rounded-lg bg-[var(--lavender-500)]">
|
||||||
<Mail className="h-5 w-5 text-brand-purple " />
|
<Mail className="h-5 w-5 text-brand-purple " />
|
||||||
@@ -287,7 +290,7 @@ const MembersDirectory = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedMember.directory_phone && (
|
{isFieldEnabled('directory_phone') && selectedMember.directory_phone && (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 rounded-lg bg-[var(--lavender-500)]">
|
<div className="p-2 rounded-lg bg-[var(--lavender-500)]">
|
||||||
<Phone className="h-5 w-5 text-brand-purple " />
|
<Phone className="h-5 w-5 text-brand-purple " />
|
||||||
@@ -305,7 +308,7 @@ const MembersDirectory = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedMember.directory_address && (
|
{isFieldEnabled('directory_address') && selectedMember.directory_address && (
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="p-2 rounded-lg bg-[var(--lavender-500)]">
|
<div className="p-2 rounded-lg bg-[var(--lavender-500)]">
|
||||||
<MapPin className="h-5 w-5 text-brand-purple " />
|
<MapPin className="h-5 w-5 text-brand-purple " />
|
||||||
@@ -319,7 +322,7 @@ const MembersDirectory = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedMember.directory_dob && (
|
{isFieldEnabled('directory_dob') && selectedMember.directory_dob && (
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 rounded-lg bg-[var(--lavender-500)]">
|
<div className="p-2 rounded-lg bg-[var(--lavender-500)]">
|
||||||
<Heart className="h-5 w-5 text-[var(--orange-light)]" />
|
<Heart className="h-5 w-5 text-[var(--orange-light)]" />
|
||||||
@@ -334,9 +337,10 @@ const MembersDirectory = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Volunteer Interests */}
|
{/* Volunteer Interests */}
|
||||||
{selectedMember.volunteer_interests && selectedMember.volunteer_interests.length > 0 && (
|
{isFieldEnabled('volunteer_interests') && selectedMember.volunteer_interests && selectedMember.volunteer_interests.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
Volunteer Interests
|
Volunteer Interests
|
||||||
@@ -355,7 +359,7 @@ const MembersDirectory = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Social Media */}
|
{/* Social Media */}
|
||||||
{(selectedMember.social_media_facebook || selectedMember.social_media_instagram ||
|
{isFieldEnabled('social_media') && (selectedMember.social_media_facebook || selectedMember.social_media_instagram ||
|
||||||
selectedMember.social_media_twitter || selectedMember.social_media_linkedin) && (
|
selectedMember.social_media_twitter || selectedMember.social_media_linkedin) && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
<h3 className="text-lg font-semibold text-[var(--purple-ink)] mb-3" style={{ fontFamily: "'Inter', sans-serif" }}>
|
||||||
|
|||||||
@@ -30,6 +30,33 @@ api.interceptors.response.use(
|
|||||||
async (error) => {
|
async (error) => {
|
||||||
const config = error.config;
|
const config = error.config;
|
||||||
|
|
||||||
|
// Handle 401 Unauthorized - session expired
|
||||||
|
if (error.response && error.response.status === 401) {
|
||||||
|
// Don't redirect if it's a login request or auth check
|
||||||
|
const isAuthRequest = config?.url?.includes('/auth/login') ||
|
||||||
|
config?.url?.includes('/auth/me') ||
|
||||||
|
config?.url?.includes('/auth/permissions');
|
||||||
|
|
||||||
|
if (!isAuthRequest) {
|
||||||
|
console.warn('[API] Session expired - redirecting to login');
|
||||||
|
|
||||||
|
// Clear auth state
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
|
||||||
|
// Dispatch custom event for components to react
|
||||||
|
window.dispatchEvent(new CustomEvent('auth:session-expired'));
|
||||||
|
|
||||||
|
// Redirect to login with session expired message
|
||||||
|
// Use replace to prevent back button issues
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
if (!currentPath.includes('/login')) {
|
||||||
|
window.location.replace('/login?session=expired');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
|
||||||
// Don't retry if we've already retried or if it's a client error (4xx)
|
// Don't retry if we've already retried or if it's a client error (4xx)
|
||||||
if (!config || config.__isRetry || (error.response && error.response.status < 500)) {
|
if (!config || config.__isRetry || (error.response && error.response.status < 500)) {
|
||||||
console.error('[API] Request failed:', {
|
console.error('[API] Request failed:', {
|
||||||
|
|||||||
Reference in New Issue
Block a user