// Get React hooks from global React object (loaded from CDN) const { useState, useMemo, useRef, useEffect } = React; // Define inline SVG icons (replaces Lucide dependency) const Search = (props) => ( ); const Globe = (props) => ( ); const Users = (props) => ( ); const ChevronDown = (props) => ( ); const MapPin = (props) => ( ); const Calendar = (props) => ( ); const X = (props) => ( ); const User = (props) => ( ); const BSFGroupFinder = () => { const [selectedDayTimes, setSelectedDayTimes] = useState([]); const [showTimeGrid, setShowTimeGrid] = useState(false); const [genderFilter, setGenderFilter] = useState('All'); const [languageFilter, setLanguageFilter] = useState(''); const [ageFilter, setAgeFilter] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const [searchMode, setSearchMode] = useState('byTime'); // 'byTime' or 'byName' const [nameSearch, setNameSearch] = useState(''); // For "By Name" mode input const [activeNameSearch, setActiveNameSearch] = useState(''); // Activated name search const [activeSearchMode, setActiveSearchMode] = useState('byTime'); // Mode when search was clicked const [showOptionsModal, setShowOptionsModal] = useState(false); const [tabDesign, setTabDesign] = useState('separator'); // 'underline', 'separator', 'pills', 'compact', 'radio', 'attached', 'minimal' const [groupsPerPage, setGroupsPerPage] = useState(10); const [showJoinModal, setShowJoinModal] = useState(false); const [joinModalType, setJoinModalType] = useState(''); // 'join', 'waitlist', 'waitlist-anytime' const [joinFlowStep, setJoinFlowStep] = useState('initial'); // 'initial', 'login', 'confirmation', 'removed' const [selectedGroupForJoin, setSelectedGroupForJoin] = useState(null); // Group being joined const [loginEmail, setLoginEmail] = useState(''); const [loginPassword, setLoginPassword] = useState(''); const [tabsInPopup, setTabsInPopup] = useState(true); const [showFilterField, setShowFilterField] = useState(false); const [showGroupCounts, setShowGroupCounts] = useState(false); // Show counts in section titles const [showAgeFilter, setShowAgeFilter] = useState(false); // Show age filter in search card const [genderSelectorStyle, setGenderSelectorStyle] = useState('filled'); // 'filled', 'outline', 'underline', 'pill', 'minimal', 'segmented' const [dayBadgeStyle, setDayBadgeStyle] = useState('circleFilled'); // 'rounded', 'circle', 'circleFilled', 'minimal', 'underline', 'dot' const [circleBorderWidth, setCircleBorderWidth] = useState('medium'); // 'thin', 'medium', 'thick' const [timeZone, setTimeZone] = useState('America/Denver (US)'); const [pendingTimeZone, setPendingTimeZone] = useState('America/Denver (US)'); // For timezone modal const [displayCount, setDisplayCount] = useState(10); const [sortBy, setSortBy] = useState('day'); const [sortOrder, setSortOrder] = useState('asc'); const [selectedAgeGroupsFilter, setSelectedAgeGroupsFilter] = useState([]); // Age group filter in sort modal const [showSortMenu, setShowSortMenu] = useState(false); const [showSortModal, setShowSortModal] = useState(false); const [maxGroupSize, setMaxGroupSize] = useState(20); const [waitlistResult, setWaitlistResult] = useState(null); const [languageSearchTerm, setLanguageSearchTerm] = useState(''); const [showLanguageDropdown, setShowLanguageDropdown] = useState(false); const [showSearchModeModal, setShowSearchModeModal] = useState(false); // Modal when switching to "By Name" with filters const [showHamburgerMenu, setShowHamburgerMenu] = useState(false); // Hamburger menu on mobile const [showLanguageFullscreen, setShowLanguageFullscreen] = useState(false); // Fullscreen language picker on mobile const [isMobile, setIsMobile] = useState(false); // Track mobile viewport // Active filters (actually applied to results) const [activeGenderFilter, setActiveGenderFilter] = useState('All'); const [activeLanguageFilter, setActiveLanguageFilter] = useState(''); const [activeAgeFilter, setActiveAgeFilter] = useState(''); const [activeSelectedDayTimes, setActiveSelectedDayTimes] = useState([]); const [activeSearchTerm, setActiveSearchTerm] = useState(''); const [hasSearched, setHasSearched] = useState(false); const [isLoading, setIsLoading] = useState(false); const [showTimezoneModal, setShowTimezoneModal] = useState(false); const timeGridRef = useRef(null); const sortMenuRef = useRef(null); const languageDropdownRef = useRef(null); const languageSearchInputRef = useRef(null); const resultsRef = useRef(null); // For scrolling to results on mobile const languageFullscreenSearchRef = useRef(null); // For fullscreen language search // Track mobile viewport useEffect(() => { const checkMobile = () => setIsMobile(window.innerWidth <= 768); checkMobile(); window.addEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkMobile); }, []); // Get timezone pin position and display info const getTimezoneInfo = (tz) => { const timezones = { // Americas 'America/Denver (US)': { top: '43%', left: '20%', display: 'Denver, CO - United States', zone: 'Mountain Time (GMT-7)', offset: -7 }, 'America/New_York (US)': { top: '42%', left: '25%', display: 'New York, NY - United States', zone: 'Eastern Time (GMT-5)', offset: -5 }, 'America/Los_Angeles (US)': { top: '44%', left: '15%', display: 'Los Angeles, CA - United States', zone: 'Pacific Time (GMT-8)', offset: -8 }, 'America/Chicago (US)': { top: '42%', left: '22%', display: 'Chicago, IL - United States', zone: 'Central Time (GMT-6)', offset: -6 }, 'America/Sao_Paulo (BR)': { top: '68%', left: '32%', display: 'São Paulo - Brazil', zone: 'Brasília Time (GMT-3)', offset: -3 }, 'America/Mexico_City (MX)': { top: '52%', left: '18%', display: 'Mexico City - Mexico', zone: 'Central Time (GMT-6)', offset: -6 }, // Europe 'Europe/London (UK)': { top: '33%', left: '47%', display: 'London - United Kingdom', zone: 'Greenwich Mean Time (GMT+0)', offset: 0 }, 'Europe/Paris (FR)': { top: '36%', left: '49%', display: 'Paris - France', zone: 'Central European Time (GMT+1)', offset: 1 }, 'Europe/Berlin (DE)': { top: '34%', left: '51%', display: 'Berlin - Germany', zone: 'Central European Time (GMT+1)', offset: 1 }, 'Europe/Moscow (RU)': { top: '30%', left: '58%', display: 'Moscow - Russia', zone: 'Moscow Time (GMT+3)', offset: 3 }, // Africa 'Africa/Casablanca (MA)': { top: '48%', left: '45%', display: 'Casablanca - Morocco', zone: 'Western European Time (GMT+0)', offset: 0 }, 'Africa/Cairo (EG)': { top: '48%', left: '54%', display: 'Cairo - Egypt', zone: 'Eastern European Time (GMT+2)', offset: 2 }, 'Africa/Lagos (NG)': { top: '55%', left: '50%', display: 'Lagos - Nigeria', zone: 'West Africa Time (GMT+1)', offset: 1 }, 'Africa/Johannesburg (ZA)': { top: '72%', left: '54%', display: 'Johannesburg - South Africa', zone: 'South Africa Time (GMT+2)', offset: 2 }, // Asia 'Asia/Hong_Kong (HK)': { top: '46%', left: '80%', display: 'Hong Kong', zone: 'Hong Kong Time (GMT+8)', offset: 8 }, 'Asia/Tokyo (JP)': { top: '40%', left: '85%', display: 'Tokyo - Japan', zone: 'Japan Standard Time (GMT+9)', offset: 9 }, 'Asia/Singapore (SG)': { top: '58%', left: '78%', display: 'Singapore', zone: 'Singapore Time (GMT+8)', offset: 8 }, 'Asia/Dubai (AE)': { top: '48%', left: '62%', display: 'Dubai - UAE', zone: 'Gulf Standard Time (GMT+4)', offset: 4 }, 'Asia/Kolkata (IN)': { top: '50%', left: '68%', display: 'Mumbai - India', zone: 'India Standard Time (GMT+5:30)', offset: 5.5 }, 'Asia/Seoul (KR)': { top: '40%', left: '82%', display: 'Seoul - South Korea', zone: 'Korea Standard Time (GMT+9)', offset: 9 }, // Oceania 'Australia/Sydney (AU)': { top: '75%', left: '88%', display: 'Sydney - Australia', zone: 'Australian Eastern Time (GMT+10)', offset: 10 }, 'Pacific/Auckland (NZ)': { top: '80%', left: '95%', display: 'Auckland - New Zealand', zone: 'New Zealand Time (GMT+12)', offset: 12 } }; return timezones[tz] || timezones['America/Denver (US)']; }; // Convert GMT time to selected timezone const convertGMTToTimezone = (dayGMT, timeGMT24h, targetTimezone) => { const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; const dayIndex = daysOfWeek.indexOf(dayGMT); if (dayIndex === -1 || !timeGMT24h) { return { day: dayGMT, time: 'TBD', time24h: '00:00' }; } // Parse GMT time (24-hour format HH:MM) const [hours, minutes] = timeGMT24h.split(':').map(Number); // Get timezone offset const offset = getTimezoneInfo(targetTimezone).offset || 0; // Apply offset let newHours = hours + offset; let newDayIndex = dayIndex; // Handle day transitions while (newHours >= 24) { newHours -= 24; newDayIndex = (newDayIndex + 1) % 7; } while (newHours < 0) { newHours += 24; newDayIndex = (newDayIndex - 1 + 7) % 7; } // Convert to 12-hour format const period = newHours >= 12 ? 'PM' : 'AM'; const hours12 = newHours === 0 ? 12 : (newHours > 12 ? newHours - 12 : newHours); const time12h = `${hours12}:${String(minutes).padStart(2, '0')} ${period}`; const time24h = `${String(newHours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; return { day: daysOfWeek[newDayIndex], time: time12h, time24h: time24h }; }; const handleSearch = () => { // Check if searching by name with filters set - show confirmation modal // Only show modal if user has actually entered a search term if (searchMode === 'byName' && nameSearch.trim() !== '') { const hasFilters = genderFilter !== 'All' || languageFilter !== '' || selectedDayTimes.length > 0; if (hasFilters) { setShowSearchModeModal(true); return; } } performSearch(); }; // Actually perform the search const performSearch = (clearFilters = false) => { setIsLoading(true); setSelectedAgeGroupsFilter([]); // Clear age filter on new search // Simulate search delay setTimeout(() => { if (clearFilters) { setActiveGenderFilter('All'); setActiveLanguageFilter(''); setActiveSelectedDayTimes([]); } else { setActiveGenderFilter(genderFilter); setActiveLanguageFilter(languageFilter); setActiveSelectedDayTimes(selectedDayTimes); } setActiveAgeFilter(ageFilter); setActiveSearchTerm(searchTerm); setActiveNameSearch(nameSearch); setActiveSearchMode(searchMode); setHasSearched(true); setIsLoading(false); setDisplayCount(groupsPerPage); // Reset pagination setShowTimeGrid(false); // Close the popup // Scroll to results on mobile if (window.innerWidth <= 768 && resultsRef.current) { setTimeout(() => { resultsRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 100); } }, 800); }; // Search by name only - clear filters and search const searchByNameOnly = () => { setGenderFilter('All'); setLanguageFilter(''); setSelectedDayTimes([]); setShowSearchModeModal(false); performSearch(true); }; // Search by name with current filters const searchByNameWithFilters = () => { setShowSearchModeModal(false); performSearch(false); }; // Show all groups matching gender/language at any time const showAllTimesForGenderLanguage = () => { setIsLoading(true); setSelectedDayTimes([]); // Clear time selections in UI setTimeout(() => { setActiveSelectedDayTimes([]); // Clear active time filter setHasSearched(true); setIsLoading(false); setDisplayCount(groupsPerPage); }, 400); }; const handleStartOver = () => { // Reset all filters setGenderFilter('All'); setLanguageFilter(''); setAgeFilter(''); setSelectedDayTimes([]); setSearchTerm(''); setNameSearch(''); // Reset active filters setActiveGenderFilter('All'); setActiveLanguageFilter(''); setActiveAgeFilter(''); setActiveSelectedDayTimes([]); setActiveSearchTerm(''); setActiveNameSearch(''); // Reset search state setHasSearched(false); setIsLoading(false); // Reset pagination setAvailableGroupsToShow(20); setFullGroupsToShow(20); }; // Auto-refresh results when timezone changes (if user has already searched) // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { if (hasSearched) { // Trigger immediate update of active filters to refresh with new timezone setActiveGenderFilter(genderFilter); setActiveLanguageFilter(languageFilter); setActiveAgeFilter(ageFilter); setActiveSelectedDayTimes([...selectedDayTimes]); setActiveSearchTerm(searchTerm); setActiveNameSearch(nameSearch); setActiveSearchMode(searchMode); } }, [timeZone]); const hasFiltersChanged = () => { if (!hasSearched) return false; const dayTimesChanged = JSON.stringify(selectedDayTimes.sort()) !== JSON.stringify(activeSelectedDayTimes.sort()); return genderFilter !== activeGenderFilter || languageFilter !== activeLanguageFilter || ageFilter !== activeAgeFilter || dayTimesChanged || searchTerm !== activeSearchTerm || nameSearch !== activeNameSearch || searchMode !== activeSearchMode; }; const getWaitlistCriteriaText = () => { const parts = []; // Day/Time if (activeSelectedDayTimes.length > 0) { const dayTimeGroups = {}; activeSelectedDayTimes.forEach(dt => { const [day, time] = dt.split('-'); if (!dayTimeGroups[day]) dayTimeGroups[day] = []; dayTimeGroups[day].push(time); }); const dayTexts = Object.entries(dayTimeGroups).map(([day, times]) => { const dayFull = { 'SUN': 'Sunday', 'MON': 'Monday', 'TUE': 'Tuesday', 'WED': 'Wednesday', 'THU': 'Thursday', 'FRI': 'Friday', 'SAT': 'Saturday' }[day]; const timeRange = times.length > 1 ? `${times[0]} and ${times[1]}` : times[0] === 'Morning' ? 'Morning (6 AM to noon)' : times[0] === 'Midday' ? 'Midday (noon to 6 PM)' : times[0] === 'Evening' ? 'Evening (6 PM to midnight)' : 'Night (midnight to 6 AM)'; return `${dayFull}, ${timeRange}`; }); parts.push(dayTexts[0]); // Just use first one for simplicity } // Gender if (activeGenderFilter && activeGenderFilter !== 'All') { parts.push(activeGenderFilter); } else { parts.push('All Genders'); } // Language if (activeLanguageFilter && activeLanguageFilter !== 'All' && activeLanguageFilter !== '') { parts.push(activeLanguageFilter); } else { parts.push('All Languages'); } // Age if (activeAgeFilter && activeAgeFilter !== 'All' && activeAgeFilter !== '') { parts.push(activeAgeFilter); } else { parts.push('All Ages'); } return parts.join(', '); }; useEffect(() => { const handleClickOutside = (event) => { if (timeGridRef.current && !timeGridRef.current.contains(event.target)) { setShowTimeGrid(false); } if (sortMenuRef.current && !sortMenuRef.current.contains(event.target)) { setShowSortMenu(false); } if (languageDropdownRef.current && !languageDropdownRef.current.contains(event.target)) { setShowLanguageDropdown(false); } }; if (showTimeGrid || showSortMenu || showSortModal || showLanguageDropdown) { document.addEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [showTimeGrid, showSortMenu, showSortModal, showLanguageDropdown]); // Auto-focus language search input when dropdown opens useEffect(() => { if (showLanguageDropdown && languageSearchInputRef.current) { setTimeout(() => { languageSearchInputRef.current.focus(); }, 50); } }, [showLanguageDropdown]); const daysOfWeek = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']; const timePeriods = [ { label: 'Morning', time: '6 AM to Noon' }, { label: 'Midday', time: 'Noon to 6 PM' }, { label: 'Evening', time: '6 PM to Midnight' }, { label: 'Night', time: 'Midnight to 6 AM' } ]; const getDaypartIcon = (period, size = 16, isSelected = false) => { const iconColor = isSelected ? "#ffffff" : "#000000"; // Morning and Midday = Sun if (period === 'Morning' || period === 'Midday') { return ( ); } // Evening and Night = Moon if (period === 'Evening' || period === 'Night') { return ( ); } return null; }; const toggleDayTime = (day, period) => { const key = `${day}-${period}`; setSelectedDayTimes(prev => prev.includes(key) ? prev.filter(item => item !== key) : [...prev, key] ); }; // Toggle all dayparts for a specific day (e.g., select all Sunday slots) const toggleAllDaypartsForDay = (day) => { const daySlots = timePeriods.map(period => `${day}-${period.label}`); const allSelected = daySlots.every(slot => selectedDayTimes.includes(slot)); if (allSelected) { // Deselect all slots for this day setSelectedDayTimes(prev => prev.filter(slot => !daySlots.includes(slot))); } else { // Select all slots for this day setSelectedDayTimes(prev => { const newSlots = daySlots.filter(slot => !prev.includes(slot)); return [...prev, ...newSlots]; }); } }; // Toggle all days for a specific daypart (e.g., select all Morning slots) const toggleAllDaysForDaypart = (daypart) => { const daypartSlots = daysOfWeek.map(day => `${day}-${daypart}`); const allSelected = daypartSlots.every(slot => selectedDayTimes.includes(slot)); if (allSelected) { // Deselect all slots for this daypart setSelectedDayTimes(prev => prev.filter(slot => !daypartSlots.includes(slot))); } else { // Select all slots for this daypart setSelectedDayTimes(prev => { const newSlots = daypartSlots.filter(slot => !prev.includes(slot)); return [...prev, ...newSlots]; }); } }; const clearSelections = () => { setSelectedDayTimes([]); }; const getSelectedTimesText = () => { if (selectedDayTimes.length === 0) return ''; // Group by day const grouped = selectedDayTimes.reduce((acc, item) => { const [day, period] = item.split('-'); if (!acc[day]) acc[day] = []; acc[day].push(period); return acc; }, {}); const parts = []; for (const [day, periods] of Object.entries(grouped)) { // Get period names (capitalize first letter) const periodNames = periods.map(p => p.charAt(0).toUpperCase() + p.slice(1) + 's'); parts.push(`${day} ${periodNames.join(', ')}`); } // Show first 2 items, then "+X more" if needed if (parts.length <= 2) { return parts.join(', '); } else { const shown = parts.slice(0, 2).join(', '); const remaining = parts.length - 2; return `${shown} +${remaining} more`; } }; // Format active selected times for waitlist description const getActiveTimesDescription = () => { if (activeSelectedDayTimes.length === 0) return 'Any day/time'; // Group by day const grouped = activeSelectedDayTimes.reduce((acc, item) => { const [day, period] = item.split('-'); if (!acc[day]) acc[day] = []; acc[day].push(period); return acc; }, {}); const parts = []; for (const [day, periods] of Object.entries(grouped)) { const periodNames = periods.map(p => p.charAt(0).toUpperCase() + p.slice(1)); parts.push(`${day} (${periodNames.join(', ')})`); } return parts.join(', '); }; const matchesSelectedDayTime = (group) => { if (activeSelectedDayTimes.length === 0) return true; const dayMap = { 'Sunday': 'SUN', 'Monday': 'MON', 'Tuesday': 'TUE', 'Wednesday': 'WED', 'Thursday': 'THU', 'Friday': 'FRI', 'Saturday': 'SAT' }; const groupDay = dayMap[group.day]; // Determine time period based on time let groupPeriod = ''; const hour = parseInt(group.time.split(':')[0]); const isPM = group.time.includes('PM'); const hour24 = isPM && hour !== 12 ? hour + 12 : (!isPM && hour === 12 ? 0 : hour); if (hour24 >= 6 && hour24 < 12) groupPeriod = 'Morning'; else if (hour24 >= 12 && hour24 < 18) groupPeriod = 'Midday'; else if (hour24 >= 18 && hour24 < 24) groupPeriod = 'Evening'; else groupPeriod = 'Night'; const groupDayTime = `${groupDay}-${groupPeriod}`; return activeSelectedDayTimes.includes(groupDayTime); }; const sortGroups = (groups) => { const dayOrder = { 'Sunday': 0, 'Monday': 1, 'Tuesday': 2, 'Wednesday': 3, 'Thursday': 4, 'Friday': 5, 'Saturday': 6 }; const convertTo24Hour = (time) => { const [timeStr, period] = time.split(' '); let [hours, minutes] = timeStr.split(':').map(Number); if (period === 'PM' && hours !== 12) hours += 12; if (period === 'AM' && hours === 12) hours = 0; return hours * 60 + minutes; }; const sorted = [...groups].sort((a, b) => { let result = 0; switch(sortBy) { case 'day': // Sort by day, then by time if (dayOrder[a.day] !== dayOrder[b.day]) { result = dayOrder[a.day] - dayOrder[b.day]; } else { result = convertTo24Hour(a.time) - convertTo24Hour(b.time); } break; case 'time': // Sort by time, then by day const timeCompare = convertTo24Hour(a.time) - convertTo24Hour(b.time); if (timeCompare !== 0) { result = timeCompare; } else { result = dayOrder[a.day] - dayOrder[b.day]; } break; case 'language': // Sort by language, then by day, then by time if (a.language !== b.language) { result = a.language.localeCompare(b.language); } else if (dayOrder[a.day] !== dayOrder[b.day]) { result = dayOrder[a.day] - dayOrder[b.day]; } else { result = convertTo24Hour(a.time) - convertTo24Hour(b.time); } break; case 'age': // Sort by age group, then by day, then by time if (a.ageGroup !== b.ageGroup) { result = a.ageGroup.localeCompare(b.ageGroup); } else if (dayOrder[a.day] !== dayOrder[b.day]) { result = dayOrder[a.day] - dayOrder[b.day]; } else { result = convertTo24Hour(a.time) - convertTo24Hour(b.time); } break; default: result = 0; } // Apply sort order (ascending or descending) return sortOrder === 'desc' ? -result : result; }); return sorted; }; // Mock group data // FOR PRODUCTION: Replace this array with your 650 groups from bsf-groups-data.js or bsf-groups-data.json // See DATA-IMPORT-GUIDE.md for instructions on importing from Excel const mockGroups = [ // Sunday groups { id: 1, time: '6:30 PM', day: 'Sunday', name: 'Sunday Evening Fellowship', gender: 'All', ageGroup: 'Mixed Ages', language: 'English', leader: 'John Smith', members: 16, waitlist: 0 }, { id: 2, time: '9:00 AM', day: 'Sunday', name: 'Sunday Morning Study', gender: 'Female', ageGroup: 'Adults', language: 'English', leader: 'Sarah Johnson', members: 14, waitlist: 0 }, { id: 3, time: '7:00 PM', day: 'Sunday', name: 'Grupo Dominical', gender: 'All', ageGroup: 'Mixed Ages', language: 'Español', leader: 'Maria Rodriguez', members: 12, waitlist: 0 }, { id: 4, time: '10:30 AM', day: 'Sunday', name: 'Late Morning Group', gender: 'Male', ageGroup: 'Young Adults', language: 'English', leader: 'Mike Davis', members: 8, waitlist: 0 }, { id: 5, time: '8:00 PM', day: 'Sunday', name: 'Evening Explorers', gender: 'All', ageGroup: 'Young Adults', language: 'English', leader: 'Emily Chen', members: 20, waitlist: 2 }, { id: 51, time: '12:30 AM', day: 'Sunday', name: 'Late Night Seekers', gender: 'All', ageGroup: 'Young Adults', language: 'English', leader: 'Nathan Pierce', members: 5, waitlist: 0 }, { id: 52, time: '2:00 AM', day: 'Sunday', name: 'Midnight Warriors', gender: 'Male', ageGroup: 'Adults', language: 'English', leader: 'Marcus Cole', members: 6, waitlist: 0 }, // Monday groups { id: 6, time: '6:00 PM', day: 'Monday', name: 'Monday Night Study', gender: 'Male', ageGroup: 'Mixed Ages', language: 'English', leader: 'Robert Wilson', members: 15, waitlist: 0 }, { id: 7, time: '9:30 AM', day: 'Monday', name: 'Morning Moms', gender: 'Female', ageGroup: 'Adults', language: 'English', leader: 'Jennifer Brown', members: 18, waitlist: 3 }, { id: 8, time: '7:30 PM', day: 'Monday', name: 'Lunes de Estudio', gender: 'All', ageGroup: 'Mixed Ages', language: 'Español', leader: 'Carlos Martinez', members: 10, waitlist: 0 }, { id: 9, time: '12:00 PM', day: 'Monday', name: 'Lunch Hour Study', gender: 'All', ageGroup: 'Mixed Ages', language: 'English', leader: 'David Lee', members: 12, waitlist: 0 }, { id: 10, time: '10:00 PM', day: 'Monday', name: 'Night Owls', gender: 'All', ageGroup: 'Young Adults', language: 'English', leader: 'Alex Turner', members: 6, waitlist: 0 }, { id: 53, time: '1:00 AM', day: 'Monday', name: 'After Midnight', gender: 'All', ageGroup: 'Mixed Ages', language: 'English', leader: 'Jordan Hayes', members: 7, waitlist: 0 }, // Tuesday groups { id: 11, time: '8:00 AM', day: 'Tuesday', name: 'Early Risers', gender: 'Male', ageGroup: 'Adults', language: 'English', leader: 'James Anderson', members: 14, waitlist: 0 }, { id: 12, time: '6:30 PM', day: 'Tuesday', name: 'Tuesday Evening Group', gender: 'Female', ageGroup: 'Mixed Ages', language: 'English', leader: 'Lisa Thompson', members: 16, waitlist: 0 }, { id: 13, time: '9:00 AM', day: 'Tuesday', name: 'Martes Matutino', gender: 'All', ageGroup: 'Adults', language: 'Español', leader: 'Ana Garcia', members: 11, waitlist: 0 }, { id: 14, time: '1:00 PM', day: 'Tuesday', name: 'Afternoon Connection', gender: 'All', ageGroup: 'Mixed Ages', language: 'English', leader: 'Mark Williams', members: 9, waitlist: 0 }, { id: 15, time: '7:00 PM', day: 'Tuesday', name: 'Tuesday Seekers', gender: 'Male', ageGroup: 'Young Adults', language: 'English', leader: 'Chris Martin', members: 15, waitlist: 0 }, { id: 16, time: '8:30 AM', day: 'Tuesday', name: 'Morning Light', gender: 'Female', ageGroup: 'Adults', language: 'English', leader: 'Patricia Moore', members: 20, waitlist: 1 }, { id: 54, time: '3:30 AM', day: 'Tuesday', name: 'Pre-Dawn Prayer', gender: 'All', ageGroup: 'Adults', language: 'English', leader: 'Samuel Brooks', members: 4, waitlist: 0 }, // Wednesday groups { id: 17, time: '7:00 PM', day: 'Wednesday', name: 'Midweek Study', gender: 'All', ageGroup: 'Mixed Ages', language: 'English', leader: 'Thomas Taylor', members: 17, waitlist: 0 }, { id: 18, time: '9:30 AM', day: 'Wednesday', name: 'Wednesday Warriors', gender: 'Male', ageGroup: 'Adults', language: 'English', leader: 'Daniel Jackson', members: 13, waitlist: 0 }, { id: 19, time: '6:00 PM', day: 'Wednesday', name: 'Miércoles de Fe', gender: 'All', ageGroup: 'Mixed Ages', language: 'Español', leader: 'Luis Hernandez', members: 14, waitlist: 0 }, { id: 20, time: '12:30 PM', day: 'Wednesday', name: 'Midday Reflections', gender: 'Female', ageGroup: 'Adults', language: 'English', leader: 'Nancy White', members: 10, waitlist: 0 }, { id: 21, time: '8:00 PM', day: 'Wednesday', name: 'Evening Fellowship', gender: 'All', ageGroup: 'Young Adults', language: 'English', leader: 'Ryan Harris', members: 18, waitlist: 0 }, { id: 22, time: '10:00 AM', day: 'Wednesday', name: 'Mid Morning Group', gender: 'Female', ageGroup: 'Mixed Ages', language: 'English', leader: 'Barbara Clark', members: 12, waitlist: 0 }, { id: 55, time: '2:30 AM', day: 'Wednesday', name: 'Night Shift Fellowship', gender: 'All', ageGroup: 'Mixed Ages', language: 'English', leader: 'Diana Reed', members: 8, waitlist: 0 }, // Thursday groups { id: 23, time: '6:30 PM', day: 'Thursday', name: 'Thursday Gathering', gender: 'Male', ageGroup: 'Mixed Ages', language: 'English', leader: 'Kevin Lewis', members: 16, waitlist: 0 }, { id: 24, time: '9:00 AM', day: 'Thursday', name: 'Thursday Morning Circle', gender: 'Female', ageGroup: 'Adults', language: 'English', leader: 'Michelle Robinson', members: 19, waitlist: 0 }, { id: 25, time: '7:30 PM', day: 'Thursday', name: 'Jueves Unidos', gender: 'All', ageGroup: 'Mixed Ages', language: 'Español', leader: 'Jorge Lopez', members: 13, waitlist: 0 }, { id: 26, time: '11:00 AM', day: 'Thursday', name: 'Late Morning Fellowship', gender: 'All', ageGroup: 'Adults', language: 'English', leader: 'Steven Walker', members: 11, waitlist: 0 }, { id: 27, time: '8:30 PM', day: 'Thursday', name: 'Evening Explorers', gender: 'All', ageGroup: 'Young Adults', language: 'English', leader: 'Jessica Hall', members: 20, waitlist: 4 }, { id: 28, time: '6:00 AM', day: 'Thursday', name: 'Dawn Patrol', gender: 'Male', ageGroup: 'Adults', language: 'English', leader: 'Brian Allen', members: 7, waitlist: 0 }, { id: 56, time: '1:30 AM', day: 'Thursday', name: 'Midnight Study', gender: 'All', ageGroup: 'Young Adults', language: 'English', leader: 'Tyler Morgan', members: 9, waitlist: 0 }, // Friday groups { id: 29, time: '7:00 PM', day: 'Friday', name: 'Friday Night Lights', gender: 'All', ageGroup: 'Young Adults', language: 'English', leader: 'Ashley Young', members: 22, waitlist: 0 }, { id: 30, time: '9:30 AM', day: 'Friday', name: 'Friday Fellowship', gender: 'Female', ageGroup: 'Mixed Ages', language: 'English', leader: 'Melissa King', members: 14, waitlist: 0 }, { id: 31, time: '6:30 PM', day: 'Friday', name: 'Viernes de Gloria', gender: 'All', ageGroup: 'Mixed Ages', language: 'Español', leader: 'Ricardo Diaz', members: 16, waitlist: 0 }, { id: 32, time: '12:00 PM', day: 'Friday', name: 'Friday Noon Group', gender: 'Male', ageGroup: 'Adults', language: 'English', leader: 'Matthew Wright', members: 9, waitlist: 0 }, { id: 33, time: '8:00 PM', day: 'Friday', name: 'TGIF Study', gender: 'All', ageGroup: 'Mixed Ages', language: 'English', leader: 'Amanda Scott', members: 17, waitlist: 0 }, { id: 34, time: '10:00 AM', day: 'Friday', name: 'Friday Morning Group', gender: 'Female', ageGroup: 'Adults', language: 'English', leader: 'Karen Green', members: 21, waitlist: 3 }, { id: 57, time: '11:30 PM', day: 'Friday', name: 'Late Friday Night', gender: 'All', ageGroup: 'Young Adults', language: 'English', leader: 'Derek Stone', members: 12, waitlist: 0 }, { id: 58, time: '3:00 AM', day: 'Friday', name: 'Early Saturday Morning', gender: 'All', ageGroup: 'Mixed Ages', language: 'English', leader: 'Casey Bennett', members: 6, waitlist: 0 }, // Saturday groups { id: 35, time: '10:00 AM', day: 'Saturday', name: 'Weekend Warriors', gender: 'All', ageGroup: 'Mixed Ages', language: 'English', leader: 'Timothy Baker', members: 18, waitlist: 0 }, { id: 36, time: '7:00 PM', day: 'Saturday', name: 'Saturday Evening Study', gender: 'Male', ageGroup: 'Adults', language: 'English', leader: 'Andrew Adams', members: 13, waitlist: 0 }, { id: 37, time: '9:00 AM', day: 'Saturday', name: 'Sábado Santo', gender: 'All', ageGroup: 'Mixed Ages', language: 'Español', leader: 'Sofia Perez', members: 15, waitlist: 0 }, { id: 38, time: '1:00 PM', day: 'Saturday', name: 'Saturday Afternoon', gender: 'Female', ageGroup: 'Young Adults', language: 'English', leader: 'Rachel Nelson', members: 12, waitlist: 0 }, { id: 39, time: '6:00 PM', day: 'Saturday', name: 'Saturday Seekers', gender: 'All', ageGroup: 'Mixed Ages', language: 'English', leader: 'Joshua Carter', members: 19, waitlist: 0 }, { id: 40, time: '8:00 AM', day: 'Saturday', name: 'Early Saturday', gender: 'Male', ageGroup: 'Adults', language: 'English', leader: 'Brandon Mitchell', members: 10, waitlist: 0 }, { id: 59, time: '12:00 AM', day: 'Saturday', name: 'Saturday Midnight', gender: 'All', ageGroup: 'Young Adults', language: 'English', leader: 'Morgan Chase', members: 11, waitlist: 0 }, { id: 60, time: '4:00 AM', day: 'Saturday', name: 'Before Dawn', gender: 'All', ageGroup: 'Adults', language: 'English', leader: 'Adrian Wells', members: 5, waitlist: 0 }, // Additional variety { id: 41, time: '11:30 AM', day: 'Sunday', name: 'Late Morning Worship', gender: 'All', ageGroup: 'Adults', language: 'English', leader: 'Gregory Perez', members: 16, waitlist: 0 }, { id: 42, time: '2:00 PM', day: 'Monday', name: 'Monday Afternoon', gender: 'Female', ageGroup: 'Adults', language: 'English', leader: 'Donna Roberts', members: 11, waitlist: 0 }, { id: 43, time: '5:00 PM', day: 'Tuesday', name: 'After Work Group', gender: 'All', ageGroup: 'Young Adults', language: 'English', leader: 'Eric Turner', members: 14, waitlist: 0 }, { id: 44, time: '3:00 PM', day: 'Wednesday', name: 'Mid Afternoon Study', gender: 'Female', ageGroup: 'Mixed Ages', language: 'English', leader: 'Carol Phillips', members: 9, waitlist: 0 }, { id: 45, time: '5:30 PM', day: 'Thursday', name: 'Dinner Hour Group', gender: 'All', ageGroup: 'Mixed Ages', language: 'English', leader: 'Paul Campbell', members: 20, waitlist: 5 }, { id: 46, time: '4:00 PM', day: 'Friday', name: 'Friday Late Afternoon', gender: 'Male', ageGroup: 'Adults', language: 'English', leader: 'Ronald Parker', members: 8, waitlist: 0 }, { id: 47, time: '11:00 AM', day: 'Saturday', name: 'Saturday Brunch Study', gender: 'All', ageGroup: 'Young Adults', language: 'English', leader: 'Nicole Evans', members: 17, waitlist: 0 }, { id: 48, time: '4:30 PM', day: 'Sunday', name: 'Sunday Late Afternoon', gender: 'Female', ageGroup: 'Adults', language: 'English', leader: 'Kimberly Edwards', members: 13, waitlist: 0 }, { id: 49, time: '10:30 PM', day: 'Friday', name: 'Late Night Study', gender: 'All', ageGroup: 'Young Adults', language: 'English', leader: 'Justin Collins', members: 6, waitlist: 0 }, { id: 50, time: '7:00 AM', day: 'Monday', name: 'Sunrise Study', gender: 'Male', ageGroup: 'Adults', language: 'English', leader: 'Raymond Stewart', members: 8, waitlist: 0 }, // Traditional Chinese groups { id: 61, time: '7:00 PM', day: 'Sunday', name: '主日晚間團契', gender: 'All', ageGroup: 'Mixed Ages', language: 'Traditional Chinese', leader: 'Li Wei Chen', members: 18, waitlist: 0 }, { id: 62, time: '9:30 AM', day: 'Monday', name: '週一早晨研讀', gender: 'Female', ageGroup: 'Adults', language: 'Traditional Chinese', leader: 'Mei Ling Wong', members: 15, waitlist: 0 }, { id: 63, time: '6:30 PM', day: 'Tuesday', name: '週二晚間小組', gender: 'All', ageGroup: 'Young Adults', language: 'Traditional Chinese', leader: 'David Chang', members: 20, waitlist: 2 }, { id: 64, time: '8:00 AM', day: 'Wednesday', name: '週三清晨禱告', gender: 'Male', ageGroup: 'Adults', language: 'Traditional Chinese', leader: 'James Liu', members: 12, waitlist: 0 }, { id: 65, time: '7:30 PM', day: 'Thursday', name: '週四查經班', gender: 'All', ageGroup: 'Mixed Ages', language: 'Traditional Chinese', leader: 'Grace Lin', members: 16, waitlist: 0 }, { id: 66, time: '10:00 AM', day: 'Friday', name: '週五團契', gender: 'Female', ageGroup: 'Adults', language: 'Traditional Chinese', leader: 'Susan Huang', members: 14, waitlist: 0 }, { id: 67, time: '2:00 PM', day: 'Saturday', name: '週六午後小組', gender: 'All', ageGroup: 'Young Adults', language: 'Traditional Chinese', leader: 'Kevin Wu', members: 11, waitlist: 0 }, { id: 68, time: '9:00 AM', day: 'Sunday', name: '主日早晨敬拜', gender: 'All', ageGroup: 'Mixed Ages', language: 'Traditional Chinese', leader: 'Peter Tsai', members: 22, waitlist: 0 }, { id: 69, time: '1:00 PM', day: 'Wednesday', name: '週三午間研讀', gender: 'All', ageGroup: 'Adults', language: 'Traditional Chinese', leader: 'Amy Chen', members: 10, waitlist: 0 }, { id: 70, time: '8:00 PM', day: 'Friday', name: '週五晚間團契', gender: 'All', ageGroup: 'Mixed Ages', language: 'Traditional Chinese', leader: 'Daniel Lee', members: 19, waitlist: 0 }, // Mandarin Chinese groups { id: 71, time: '6:00 PM', day: 'Monday', name: '周一晚间学习', gender: 'All', ageGroup: 'Mixed Ages', language: 'Mandarin Chinese', leader: 'Wang Xiaomin', members: 17, waitlist: 0 }, { id: 72, time: '9:00 AM', day: 'Tuesday', name: '周二早晨团契', gender: 'Female', ageGroup: 'Adults', language: 'Mandarin Chinese', leader: 'Zhang Hua', members: 13, waitlist: 0 }, { id: 73, time: '7:00 PM', day: 'Wednesday', name: '周三晚间查经', gender: 'All', ageGroup: 'Young Adults', language: 'Mandarin Chinese', leader: 'Li Ming', members: 20, waitlist: 3 }, { id: 74, time: '10:30 AM', day: 'Thursday', name: '周四上午祷告会', gender: 'Male', ageGroup: 'Adults', language: 'Mandarin Chinese', leader: 'Chen Wei', members: 9, waitlist: 0 }, { id: 75, time: '6:30 PM', day: 'Friday', name: '周五晚间小组', gender: 'All', ageGroup: 'Mixed Ages', language: 'Mandarin Chinese', leader: 'Liu Fang', members: 16, waitlist: 0 }, { id: 76, time: '8:30 AM', day: 'Saturday', name: '周六早晨敬拜', gender: 'All', ageGroup: 'Adults', language: 'Mandarin Chinese', leader: 'Zhou Jian', members: 21, waitlist: 1 }, { id: 77, time: '7:30 PM', day: 'Sunday', name: '主日晚间团契', gender: 'All', ageGroup: 'Mixed Ages', language: 'Mandarin Chinese', leader: 'Xu Lin', members: 18, waitlist: 0 }, { id: 78, time: '11:00 AM', day: 'Monday', name: '周一午间查经', gender: 'Female', ageGroup: 'Adults', language: 'Mandarin Chinese', leader: 'Gao Mei', members: 12, waitlist: 0 }, { id: 79, time: '5:00 PM', day: 'Tuesday', name: '周二下班后小组', gender: 'All', ageGroup: 'Young Adults', language: 'Mandarin Chinese', leader: 'Sun Wei', members: 15, waitlist: 0 }, { id: 80, time: '9:30 AM', day: 'Saturday', name: '周六晨间学习', gender: 'All', ageGroup: 'Mixed Ages', language: 'Mandarin Chinese', leader: 'Ma Qiang', members: 14, waitlist: 0 }, ]; // Use external data if available (from bsf-groups-data.js), otherwise use mock data // Support both 'groupsData' and 'BSF_GROUPS_DATA' variable names const externalData = (typeof BSF_GROUPS_DATA !== 'undefined' && BSF_GROUPS_DATA.length > 0) ? BSF_GROUPS_DATA : (typeof groupsData !== 'undefined' && groupsData.length > 0) ? groupsData : null; const rawGroups = externalData || mockGroups; // Convert all groups to selected timezone const allGroups = useMemo(() => { return rawGroups.map(group => { // If group has GMT data, convert it if (group.timeGMT24h && group.dayGMT) { const converted = convertGMTToTimezone(group.dayGMT, group.timeGMT24h, timeZone); return { ...group, day: converted.day, time: converted.time, time24h: converted.time24h }; } // Otherwise use existing day/time (for mock data) return group; }); }, [rawGroups, timeZone]); const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; // Extract unique languages and age groups from actual data const uniqueLanguages = useMemo(() => { const langs = [...new Set(allGroups.map(g => g.language).filter(Boolean))]; // Fallback to common languages if extraction fails if (langs.length === 0) { return ['English', 'Spanish', 'Mandarin Chinese', 'Cantonese', 'Arabic', 'French', 'Portuguese', 'Korean', 'Hindi', 'Russian']; } return langs.sort(); }, [allGroups]); const uniqueAgeGroups = useMemo(() => { const ages = [...new Set(allGroups.map(g => g.ageGroup).filter(Boolean))]; // Fallback to common age groups if extraction fails if (ages.length === 0) { return ['Mixed Ages', 'Young Adults', 'Adults', 'Seniors']; } return ages.sort(); }, [allGroups]); // Filter languages based on search term const filteredLanguages = useMemo(() => { if (!languageSearchTerm.trim()) return uniqueLanguages; return uniqueLanguages.filter(lang => lang.toLowerCase().includes(languageSearchTerm.toLowerCase()) ); }, [uniqueLanguages, languageSearchTerm]); // Calculate waitlist statistics const calculateWaitlistStats = () => { const groupsWithWaitlist = allGroups.filter(g => g.members > maxGroupSize); return { total: groupsWithWaitlist.length, percentage: ((groupsWithWaitlist.length / allGroups.length) * 100).toFixed(1) }; }; // Filter groups const filteredGroups = useMemo(() => { return allGroups.filter(group => { const matchesGender = activeGenderFilter === 'All' || group.gender === activeGenderFilter || group.gender === 'All'; const matchesLanguage = activeLanguageFilter === '' || activeLanguageFilter === 'All' || group.language === activeLanguageFilter; const matchesAge = activeAgeFilter === '' || activeAgeFilter === 'All' || group.ageGroup === activeAgeFilter; // Age group filter from Sort/Filter modal const matchesAgeGroupFilter = selectedAgeGroupsFilter.length === 0 || selectedAgeGroupsFilter.includes(group.ageGroup); // Live filter (from results area) - always applies const matchesLiveSearch = activeSearchTerm === '' || group.name.toLowerCase().includes(activeSearchTerm.toLowerCase()) || group.leader.toLowerCase().includes(activeSearchTerm.toLowerCase()); // Initial search filter based on mode let matchesInitialSearch = true; if (activeSearchMode === 'byName') { // In "By Name" mode, filter by name/leader from the top search matchesInitialSearch = activeNameSearch === '' || group.name.toLowerCase().includes(activeNameSearch.toLowerCase()) || group.leader.toLowerCase().includes(activeNameSearch.toLowerCase()); } else { // In "By Time" mode, filter by selected day/times matchesInitialSearch = matchesSelectedDayTime(group); } return matchesGender && matchesLanguage && matchesAge && matchesAgeGroupFilter && matchesLiveSearch && matchesInitialSearch; }); }, [activeGenderFilter, activeLanguageFilter, activeAgeFilter, activeSearchTerm, activeSelectedDayTimes, activeNameSearch, activeSearchMode, selectedAgeGroupsFilter]); const availableGroups = useMemo(() => { // Groups with members LESS than maxGroupSize are available return sortGroups(filteredGroups.filter(g => g.members < maxGroupSize)); }, [filteredGroups, sortBy, sortOrder, maxGroupSize]); const fullGroups = useMemo(() => { // Groups with members >= maxGroupSize are full (need to join waitlist) return sortGroups(filteredGroups.filter(g => g.members >= maxGroupSize)); }, [filteredGroups, sortBy, sortOrder, maxGroupSize]); // Groups matching gender and language only (ignoring time) - for "no results" suggestions const groupsMatchingGenderLanguage = useMemo(() => { return allGroups.filter(group => { const matchesGender = activeGenderFilter === 'All' || group.gender === activeGenderFilter || group.gender === 'All'; const matchesLanguage = activeLanguageFilter === '' || activeLanguageFilter === 'All' || group.language === activeLanguageFilter; return matchesGender && matchesLanguage; }); }, [allGroups, activeGenderFilter, activeLanguageFilter]); // Count groups by age group in current results (before age filter applied) for Sort/Filter modal const ageGroupCounts = useMemo(() => { // Get groups matching all filters EXCEPT age group filter const baseFilteredGroups = allGroups.filter(group => { const matchesGender = activeGenderFilter === 'All' || group.gender === activeGenderFilter || group.gender === 'All'; const matchesLanguage = activeLanguageFilter === '' || activeLanguageFilter === 'All' || group.language === activeLanguageFilter; const matchesAge = activeAgeFilter === '' || activeAgeFilter === 'All' || group.ageGroup === activeAgeFilter; const matchesLiveSearch = activeSearchTerm === '' || group.name.toLowerCase().includes(activeSearchTerm.toLowerCase()) || group.leader.toLowerCase().includes(activeSearchTerm.toLowerCase()); let matchesInitialSearch = true; if (activeSearchMode === 'byName') { matchesInitialSearch = activeNameSearch === '' || group.name.toLowerCase().includes(activeNameSearch.toLowerCase()) || group.leader.toLowerCase().includes(activeNameSearch.toLowerCase()); } else { matchesInitialSearch = matchesSelectedDayTime(group); } return matchesGender && matchesLanguage && matchesAge && matchesLiveSearch && matchesInitialSearch; }); // Count by age group const counts = {}; baseFilteredGroups.forEach(group => { counts[group.ageGroup] = (counts[group.ageGroup] || 0) + 1; }); return counts; }, [allGroups, activeGenderFilter, activeLanguageFilter, activeAgeFilter, activeSearchTerm, activeSelectedDayTimes, activeNameSearch, activeSearchMode]); const GroupCard = ({ group }) => { // Group is full if members >= maxGroupSize setting const isFull = group.members >= maxGroupSize; const dayMap = { 'Sunday': 'Sun', 'Monday': 'Mon', 'Tuesday': 'Tue', 'Wednesday': 'Wed', 'Thursday': 'Thu', 'Friday': 'Fri', 'Saturday': 'Sat' }; const groupDayShort = dayMap[group.day]; // Determine daypart from time const hour = parseInt(group.time.split(':')[0]); const isPM = group.time.includes('PM'); const hour24 = isPM && hour !== 12 ? hour + 12 : (!isPM && hour === 12 ? 0 : hour); let daypart = ''; if (hour24 >= 6 && hour24 < 12) daypart = 'Morning'; else if (hour24 >= 12 && hour24 < 18) daypart = 'Midday'; else if (hour24 >= 18 && hour24 < 24) daypart = 'Evening'; else daypart = 'Night'; return (
{getDaypartIcon(daypart, 18)} {group.time}, {group.day}
{days.map(day => ( {dayBadgeStyle === 'dot' ? (day === groupDayShort ? day : '') : day} ))}

{group.name}

{group.gender} • {group.ageGroup} • {group.members} members{isFull && group.waitlist > 0 ? `, ${group.waitlist} waitlist` : ''}
{group.language}
Leader: {group.leader} {isFull ? `${group.members} members${group.waitlist > 0 ? `, ${group.waitlist} on waitlist` : ''}` : `${group.members} members` }
); }; return (
setShowOptionsModal(true)}>
B
BSF Online
{showHamburgerMenu && (
)} {/* Options Modal */} {showOptionsModal && (
setShowOptionsModal(false)}>
e.stopPropagation()}>

Prototype Options

Groups Per Page

setGroupsPerPage(parseInt(e.target.value) || 10)} style={{ width: '100%', padding: '0.75rem', fontSize: '1rem', border: '2px solid #e9ecef', borderRadius: '8px', fontFamily: 'Roboto, sans-serif' }} />
Default: 10. Controls how many groups show before "Show More" button appears.

Interface Options

setTabsInPopup(!tabsInPopup)} style={{marginBottom: '0.75rem'}} >
Move Tabs to Popup
Place "Choose Times" / "Search by Name" tabs inside the popup instead of main search card
setShowFilterField(!showFilterField)} style={{marginBottom: '0.75rem'}} >
Show Filter Field
Display the live filter text input in search results
setShowGroupCounts(!showGroupCounts)} style={{marginBottom: '0.75rem'}} >
Show Group Counts in Section Titles
Display the number of groups in "Available Groups" and "Full Groups" headings
setShowAgeFilter(!showAgeFilter)} >
Show Age Filter
Display the age range filter in the search card

Day Badge Style

setDayBadgeStyle('rounded')} style={{marginBottom: '0.75rem'}} >
Rounded
Rounded rectangle badges with background color
setDayBadgeStyle('circle')} style={{marginBottom: '0.75rem'}} >
Circle Outline
Circular badges with border outline
{dayBadgeStyle === 'circle' && (
Circle Border Thickness
)}
setDayBadgeStyle('circleFilled')} style={{marginBottom: '0.75rem'}} >
Circle Filled (Default)
Circular badges with grey fill for inactive days
setDayBadgeStyle('minimal')} style={{marginBottom: '0.75rem'}} >
Minimal
Text only with bold for active day
setDayBadgeStyle('underline')} style={{marginBottom: '0.75rem'}} >
Underline
Text with underline for active day
setDayBadgeStyle('dot')} >
Dot Indicator
Small dots for inactive days, pill for active day

Gender Selector Style

setGenderSelectorStyle('filled')} style={{marginBottom: '0.75rem'}} >
Filled
Solid background on selected option
setGenderSelectorStyle('outline')} style={{marginBottom: '0.75rem'}} >
Outline
Bordered segments, selected fills with color
setGenderSelectorStyle('segmented')} style={{marginBottom: '0.75rem'}} >
Segmented
iOS-style segmented control with sliding selection
setGenderSelectorStyle('pill')} style={{marginBottom: '0.75rem'}} >
Pill
Separate rounded pill buttons
setGenderSelectorStyle('chips')} style={{marginBottom: '0.75rem'}} >
Chips
Chip-style buttons with subtle background
setGenderSelectorStyle('underline')} style={{marginBottom: '0.75rem'}} >
Underline
Tab-style with underline indicator
setGenderSelectorStyle('minimal')} >
Minimal
Text only with color change

Waitlist Options

setMaxGroupSize(parseInt(e.target.value) || 20)} style={{ width: '100%', padding: '0.75rem', border: '2px solid #e9ecef', borderRadius: '8px', fontSize: '1rem', fontFamily: 'Roboto, sans-serif', color: '#212529', backgroundColor: '#ffffff', boxSizing: 'border-box', WebkitAppearance: 'none', MozAppearance: 'textfield' }} />
Groups with more than this number of members will show as full with a waitlist option
{(() => { const fullCount = allGroups.filter(g => g.members >= maxGroupSize).length; return `${fullCount} of ${allGroups.length} Groups will have a Waitlist (max size: ${maxGroupSize})`; })()}

Tab Design Style

setTabDesign('underline')} >
Underline Tabs
Clean minimal design with underline indicator for active tab
setTabDesign('separator')} >
Text with Separator
Absolute minimal with "|" separator between options
setTabDesign('pills')} >
Pill Buttons
Outlined pills that fill with color when active
setTabDesign('compact')} >
Compact Segmented
Connected buttons with grey background container
setTabDesign('radio')} >
Radio Button Style
Radio circles with text labels
setTabDesign('attached')} >
Attached to Card
Tabs sit at the top edge of the search card
setTabDesign('minimal')} >
Minimal Spacing
Reduced vertical and horizontal spacing for compact look
)}

In-Depth Bible Study. Online. Worldwide.

Our Current Study: Exile & Return
{!tabsInPopup && tabDesign === 'attached' && (
)} {!tabsInPopup && tabDesign !== 'attached' && (
{tabDesign === 'radio' ? ( <> ) : ( <> {(tabDesign === 'separator' || tabDesign === 'minimal') && ( | )} )}
)}
{searchMode === 'byTime' ? ( <> { setShowTimeGrid(!showTimeGrid); setShowLanguageDropdown(false); }} readOnly /> ) : ( <> setNameSearch(e.target.value)} onClick={() => { if (tabsInPopup) { setShowTimeGrid(true); setShowLanguageDropdown(false); } }} readOnly={tabsInPopup} /> )} {showTimeGrid && (
{tabsInPopup ? (
) : (

Choose Times

)} {(!tabsInPopup || searchMode === 'byTime') && ( <> {/* Desktop Grid: Rows = Time Periods, Columns = Days */}
{daysOfWeek.map(day => (
toggleAllDaypartsForDay(day)} title={`Select all ${day}`} > {day}
))}
{timePeriods.map(period => (
toggleAllDaysForDaypart(period.label)} title={`Select all ${period.label}`} > {getDaypartIcon(period.label, 18)} {period.label} {period.time}
{daysOfWeek.map(day => ( ))}
))}
{/* Mobile Grid: Rows = Days, Columns = Time Periods */}
{timePeriods.map(period => (
toggleAllDaysForDaypart(period.label)} title={`Select all ${period.label}`} > {getDaypartIcon(period.label, 16)} {period.label} {period.time}
))}
{daysOfWeek.map(day => (
toggleAllDaypartsForDay(day)} title={`Select all ${day}`} > {day}
{timePeriods.map(period => ( ))}
))}
)} {tabsInPopup && searchMode === 'byName' && (
setNameSearch(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { setShowTimeGrid(false); handleSearch(); } }} autoFocus />

Search for groups by leader name or group name

)}
{(!tabsInPopup || searchMode === 'byTime') && ( Clear Times )}
)}
{['Female', 'Male', 'All'].map(gender => ( ))}
{ if (isMobile) { setShowLanguageFullscreen(true); setShowTimeGrid(false); setLanguageSearchTerm(''); } else { setShowLanguageDropdown(!showLanguageDropdown); if (!showLanguageDropdown) { setShowTimeGrid(false); } } }} > {languageFilter || 'Language'}
{!isMobile && showLanguageDropdown && (
{ const value = e.target.value; setLanguageSearchTerm(value); // Auto-select if exact match or only one result if (value.trim()) { const matches = uniqueLanguages.filter(lang => lang.toLowerCase().includes(value.toLowerCase()) ); const exactMatch = uniqueLanguages.find(lang => lang.toLowerCase() === value.toLowerCase() ); if (exactMatch) { setLanguageFilter(exactMatch); } else if (matches.length === 1) { setLanguageFilter(matches[0]); } } }} onKeyDown={(e) => { if (e.key === 'Enter' && filteredLanguages.length > 0) { setLanguageFilter(filteredLanguages[0]); setShowLanguageDropdown(false); setLanguageSearchTerm(''); } else if (e.key === 'Escape') { setShowLanguageDropdown(false); setLanguageSearchTerm(''); } }} onClick={(e) => e.stopPropagation()} />
{ setLanguageFilter(''); setShowLanguageDropdown(false); setLanguageSearchTerm(''); }} > All
{filteredLanguages.map((lang, index) => { // Highlight matching text const searchTerm = languageSearchTerm.toLowerCase(); const langLower = lang.toLowerCase(); const matchIndex = searchTerm ? langLower.indexOf(searchTerm) : -1; const isFirstMatch = index === 0 && languageSearchTerm.trim(); let displayContent; if (matchIndex >= 0 && searchTerm) { const before = lang.slice(0, matchIndex); const match = lang.slice(matchIndex, matchIndex + searchTerm.length); const after = lang.slice(matchIndex + searchTerm.length); displayContent = <>{before}{match}{after}; } else { displayContent = lang; } return (
{ setLanguageFilter(lang); setShowLanguageDropdown(false); setLanguageSearchTerm(''); }} > {displayContent}
); })}
)}
{showAgeFilter && (
)}
{getTimezoneInfo(timeZone).display} { e.preventDefault(); setPendingTimeZone(timeZone); // Initialize pending with current setShowTimezoneModal(true); }} > Change time zone *All displayed meeting times are converted to the selected time zone.
{hasSearched && ( Start Over )}
{hasSearched && !isLoading && (
Found {filteredGroups.length} groups
{showFilterField && ( { setSearchTerm(e.target.value); if (hasSearched) { setActiveSearchTerm(e.target.value); } }} onKeyDown={(e) => { if (e.key === 'Enter') { handleSearch(); } }} /> )}
)} {!hasSearched ? (
🔍

Ready to Find Your Group?

Select your preferred days, times, and other search criteria above, then click the Search button to discover groups that match your schedule.

) : isLoading ? (

Searching for groups...

) : ( <> {(() => { const totalGroups = availableGroups.length + fullGroups.length; // Calculate how many of each type to show based on displayCount const availableToShow = Math.min(availableGroups.length, displayCount); const remainingSlots = displayCount - availableToShow; const fullToShow = Math.min(fullGroups.length, remainingSlots); const visibleAvailable = availableGroups.slice(0, availableToShow); const visibleFull = fullGroups.slice(0, fullToShow); const totalVisible = availableToShow + fullToShow; const hasMore = totalVisible < totalGroups; return ( <> {/* Available Groups Section */} {visibleAvailable.length > 0 && (

Available Groups{showGroupCounts ? ` (${availableGroups.length})` : ''}

{visibleAvailable.map(group => ( ))}
)} {/* Full Groups Section */} {visibleFull.length > 0 && (
0 ? '2rem' : '0' }}>

Full Groups{showGroupCounts ? ` (${fullGroups.length})` : ''}

{visibleFull.map(group => ( ))}
)} {/* Show More Button */} {hasMore && (
Showing {totalVisible} of {totalGroups} groups.
)} {totalGroups === 0 && (
{activeSearchMode === 'byName' ? ( <>

No groups match your search.

{(activeGenderFilter !== 'All' || (activeLanguageFilter && activeLanguageFilter !== 'All')) ? ( <>

No groups found matching "{activeNameSearch}" with your current filters:

{activeGenderFilter !== 'All' && {activeGenderFilter}} {activeGenderFilter !== 'All' && activeLanguageFilter && activeLanguageFilter !== 'All' && ' • '} {activeLanguageFilter && activeLanguageFilter !== 'All' && {activeLanguageFilter}}

Try a different search term or adjust your filters.

) : (

No groups found matching "{activeNameSearch}". Try a different search term.

)} ) : ( <>

No groups match your search.

{groupsMatchingGenderLanguage.length > 0 ? ( <> {/* There ARE other times available - show "other times" box and 1 waitlist option */}

{groupsMatchingGenderLanguage.length} {activeGenderFilter !== 'All' ? activeGenderFilter.toLowerCase() : ''} group{groupsMatchingGenderLanguage.length !== 1 ? 's' : ''} available {activeLanguageFilter && activeLanguageFilter !== 'All' ? ` in ${activeLanguageFilter}` : ''} at other times

{/* Single waitlist option for specific times */}

Or, join a waitlist:

Join a waitlist for a {activeGenderFilter !== 'All' ? activeGenderFilter.toLowerCase() : ''} group {activeLanguageFilter && activeLanguageFilter !== 'All' ? <> in {activeLanguageFilter} : ''} {activeSelectedDayTimes.length > 0 ? <>, on {getActiveTimesDescription()} : ''}

) : ( <> {/* NO other times available - show 2 waitlist options */}

Here are your options:

{/* Option 1: Waitlist for gender and language (any time) */}

Option 1

Join a waitlist for a {activeGenderFilter !== 'All' ? activeGenderFilter.toLowerCase() : ''} group {activeLanguageFilter && activeLanguageFilter !== 'All' ? <> in {activeLanguageFilter} : ''}, at any time

{/* Option 2: Waitlist for gender, language and specific time */}

Option 2

Join a waitlist for a {activeGenderFilter !== 'All' ? activeGenderFilter.toLowerCase() : ''} group {activeLanguageFilter && activeLanguageFilter !== 'All' ? <> in {activeLanguageFilter} : ''} {activeSelectedDayTimes.length > 0 ? <>, on {getActiveTimesDescription()} : ''}

)} )}
)} ); })()} )} {/* Join Flow Modal */} {showJoinModal && ( <> {joinFlowStep === 'initial' && (
{ setShowJoinModal(false); setJoinFlowStep('initial'); }}>
e.stopPropagation()}>

{joinModalType === 'join' ? 'Join Group' : 'Join Waitlist'}

{joinModalType === 'join' ? ( <> {selectedGroupForJoin && (

You're joining:

{selectedGroupForJoin.time}, {selectedGroupForJoin.day}

{selectedGroupForJoin.name}

Leader: {selectedGroupForJoin.leader}

)}

Please login with your existing account or create a new one to join this group.

) : selectedGroupForJoin ? ( <> {/* Waitlist for a specific full group */}

You're joining the waitlist for:

{selectedGroupForJoin.time}, {selectedGroupForJoin.day}

{selectedGroupForJoin.name}

Leader: {selectedGroupForJoin.leader} • {selectedGroupForJoin.members} members

This group is currently full. Please login with your existing account or create a new one to join the waitlist. You'll be notified when a spot opens up.

) : ( <> {/* General waitlist from no-results */}

You're joining the waitlist for:

{activeGenderFilter !== 'All' ? activeGenderFilter : 'Any gender'} • {activeLanguageFilter && activeLanguageFilter !== 'All' ? activeLanguageFilter : 'Any language'}

{joinModalType === 'waitlist-anytime' ? 'Any day/time' : getActiveTimesDescription()}

Please login with your existing account or create a new one to join the waitlist. You'll be notified when a matching group becomes available.

)}
)} {joinFlowStep === 'login' && (
Bible Study Fellowship

Please login with your existing account

Sign in

setLoginEmail(e.target.value)} /> Forgot your password? setLoginPassword(e.target.value)} />

Don't have a BSF account? Create an Account

)} {joinFlowStep === 'confirmation' && (
BSF Online
{joinModalType === 'join' ? ( <>

You have joined the group:

{selectedGroupForJoin && (

{selectedGroupForJoin.time}, {selectedGroupForJoin.day}

{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(d => ( {d} ))}

{selectedGroupForJoin.name}

{selectedGroupForJoin.gender} • {selectedGroupForJoin.ageGroup} {selectedGroupForJoin.language}

Leader: {selectedGroupForJoin.leader}

{selectedGroupForJoin.members} members

)} ) : joinModalType === 'waitlist' && selectedGroupForJoin ? ( <>

You are 12 on a waitlist for the group:

{selectedGroupForJoin.time}, {selectedGroupForJoin.day}

{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(d => ( {d} ))}

{selectedGroupForJoin.name}

{selectedGroupForJoin.gender} • {selectedGroupForJoin.ageGroup} {selectedGroupForJoin.language}

Leader: {selectedGroupForJoin.leader}

{selectedGroupForJoin.members} members

) : ( <>

You are on a waitlist for a group at:

{getTimezoneInfo(timeZone).display}
Meeting Day and Time
{joinModalType === 'waitlist-anytime' ? (
{['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'].map(d => ( {d} ))}
{['6 AM to Noon', 'Noon to 6 PM', '6 PM to Midnight', 'Midnight to 6 AM'].map((time, i) => (
{time} {['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'].map(d => ( {['Morning', 'Midday', 'Evening', 'Night'][i]} ))}
))}
) : (

{getActiveTimesDescription()}

)}

{activeGenderFilter !== 'All' ? activeGenderFilter : 'Any gender'} • Mixed Ages

{activeLanguageFilter && activeLanguageFilter !== 'All' ? activeLanguageFilter : 'Any language'}

)}

Not sure? Browse through other times and groups, you won't be removed from your {joinModalType === 'join' ? 'group' : 'waitlist'}.

)} {joinFlowStep === 'removed' && (
{ setShowJoinModal(false); setJoinFlowStep('initial'); }}>
e.stopPropagation()} style={{textAlign: 'center', padding: '2.5rem 2rem'}}>

Removed from Waitlist

You have been removed from the waitlist for this group.

)} )}
{/* Search Mode Confirmation Modal */} {/* Fullscreen Language Picker (Mobile) */} {showLanguageFullscreen && (

Language

{ const value = e.target.value; setLanguageSearchTerm(value); // Auto-select if exact match or only one result (matches desktop behavior) if (value.trim()) { const matches = uniqueLanguages.filter(lang => lang.toLowerCase().includes(value.toLowerCase()) ); const exactMatch = uniqueLanguages.find(lang => lang.toLowerCase() === value.toLowerCase() ); if (exactMatch) { setLanguageFilter(exactMatch); } else if (matches.length === 1) { setLanguageFilter(matches[0]); } } }} autoFocus />
{ setLanguageFilter(''); setShowLanguageFullscreen(false); setLanguageSearchTerm(''); }} > All Languages
{filteredLanguages.map(lang => (
{ setLanguageFilter(lang); setShowLanguageFullscreen(false); setLanguageSearchTerm(''); }} > {lang}
))}
)} {showSearchModeModal && (
setShowSearchModeModal(false)}>
e.stopPropagation()}>

Search by Name

You currently have filters selected:

{genderFilter !== 'All' && {genderFilter}} {genderFilter !== 'All' && languageFilter && ' • '} {languageFilter && {languageFilter}} {(genderFilter !== 'All' || languageFilter) && selectedDayTimes.length > 0 && ' • '} {selectedDayTimes.length > 0 && {selectedDayTimes.length} time slot{selectedDayTimes.length !== 1 ? 's' : ''}}

Would you like to search by name only, or continue searching with your current filters?

)} {showTimezoneModal && (
setShowTimezoneModal(false)}>
e.stopPropagation()}>

Select Time Zone

{getTimezoneInfo(pendingTimeZone).zone}
)} {/* Sort/Filter Modal */} {showSortModal && (
setShowSortModal(false)}>
e.stopPropagation()}>

Sort / Filter

Sort By

setSortBy('day')} >
Day of Week
setSortBy('time')} >
Time of Day
setSortBy('language')} >
Language

Order

setSortOrder('asc')} >
Ascending
setSortOrder('desc')} >
Descending

Filter by Age Group

{uniqueAgeGroups.map(ageGroup => { const count = ageGroupCounts[ageGroup] || 0; const isSelected = selectedAgeGroupsFilter.includes(ageGroup); return (
{ if (isSelected) { setSelectedAgeGroupsFilter(prev => prev.filter(a => a !== ageGroup)); } else { setSelectedAgeGroupsFilter(prev => [...prev, ageGroup]); } }} >
{isSelected && }
{ageGroup}
{count}
); })}
{selectedAgeGroupsFilter.length > 0 && ( )}
)}
); }; // Component ready for use - rendered by HTML file