Merge from dev to loaf-prod for DEMO #25

Merged
andika merged 45 commits from dev into loaf-prod 2026-02-02 11:12:58 +00:00
8 changed files with 364 additions and 202 deletions
Showing only changes of commit d4acef8d90 - Show all commits

View File

@@ -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 */}

View File

@@ -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;

View File

@@ -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 && (

View File

@@ -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 = () => {

View File

@@ -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 }));

View File

@@ -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>
); );

View File

@@ -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" }}>

View File

@@ -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:', {