// 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`
}
{
setSelectedGroupForJoin(group);
setJoinModalType(isFull ? 'waitlist' : 'join');
setShowJoinModal(true);
}}
>
{isFull ? 'Join Waitlist' : 'Join'}
);
};
return (
setShowOptionsModal(true)}>
B
BSF Online
Find an Online Group
Your Account
setShowHamburgerMenu(!showHamburgerMenu)}>
{showHamburgerMenu ? (
) : (
)}
{showHamburgerMenu && (
setShowHamburgerMenu(false)}>
Find an Online Group
setShowHamburgerMenu(false)}>
Your Account
)}
{/* Options Modal */}
{showOptionsModal && (
setShowOptionsModal(false)}>
e.stopPropagation()}>
Prototype Options
setShowOptionsModal(false)}>×
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
setCircleBorderWidth('thin')}
style={{
flex: 1,
padding: '0.5rem',
border: circleBorderWidth === 'thin' ? '2px solid #0f4c5c' : '1px solid #dee2e6',
borderRadius: '6px',
background: circleBorderWidth === 'thin' ? '#f0f9fa' : 'white',
color: circleBorderWidth === 'thin' ? '#0f4c5c' : '#495057',
fontWeight: circleBorderWidth === 'thin' ? '600' : '400',
cursor: 'pointer',
fontSize: '0.8rem'
}}
>
Thin
setCircleBorderWidth('medium')}
style={{
flex: 1,
padding: '0.5rem',
border: circleBorderWidth === 'medium' ? '2px solid #0f4c5c' : '1px solid #dee2e6',
borderRadius: '6px',
background: circleBorderWidth === 'medium' ? '#f0f9fa' : 'white',
color: circleBorderWidth === 'medium' ? '#0f4c5c' : '#495057',
fontWeight: circleBorderWidth === 'medium' ? '600' : '400',
cursor: 'pointer',
fontSize: '0.8rem'
}}
>
Medium
setCircleBorderWidth('thick')}
style={{
flex: 1,
padding: '0.5rem',
border: circleBorderWidth === 'thick' ? '2px solid #0f4c5c' : '1px solid #dee2e6',
borderRadius: '6px',
background: circleBorderWidth === 'thick' ? '#f0f9fa' : 'white',
color: circleBorderWidth === 'thick' ? '#0f4c5c' : '#495057',
fontWeight: circleBorderWidth === 'thick' ? '600' : '400',
cursor: 'pointer',
fontSize: '0.8rem'
}}
>
Thick
)}
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
Maximum Discussion Group Size
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' && (
setSearchMode('byTime')}
>
Find by Time
setSearchMode('byName')}
>
Find by Name
)}
{!tabsInPopup && tabDesign !== 'attached' && (
{tabDesign === 'radio' ? (
<>
setSearchMode('byTime')}
>
Find by Time
setSearchMode('byName')}
>
Find by Name
>
) : (
<>
setSearchMode('byTime')}
>
Find by Time
{(tabDesign === 'separator' || tabDesign === 'minimal') && (
|
)}
setSearchMode('byName')}
>
Find by Name
>
)}
)}
{searchMode === 'byTime' ? (
<>
{
setShowTimeGrid(!showTimeGrid);
setShowLanguageDropdown(false);
}}
readOnly
/>
>
) : (
<>
setNameSearch(e.target.value)}
onClick={() => {
if (tabsInPopup) {
setShowTimeGrid(true);
setShowLanguageDropdown(false);
}
}}
readOnly={tabsInPopup}
/>
>
)}
{showTimeGrid && (
{tabsInPopup ? (
{
e.stopPropagation();
setSearchMode('byTime');
}}
>
Choose Times
{
e.stopPropagation();
setSearchMode('byName');
}}
>
Search by Name
setShowTimeGrid(false)}
>
×
) : (
Choose Times
setShowTimeGrid(false)}
>
×
)}
{(!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 => (
toggleDayTime(day, period.label)}
>
{period.label}
))}
))}
{/* 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 => (
toggleDayTime(day, period.label)}
>
{period.label}
))}
))}
>
)}
{tabsInPopup && searchMode === 'byName' && (
)}
{
setShowTimeGrid(false);
if (tabsInPopup && searchMode === 'byName') {
handleSearch();
}
}}
>
{tabsInPopup && searchMode === 'byName' ? 'Search' : 'Done'}
{(!tabsInPopup || searchMode === 'byTime') && (
Clear Times
)}
)}
{['Female', 'Male', 'All'].map(gender => (
setGenderFilter(gender)}
>
{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 && (
setAgeFilter(e.target.value)}
>
Age Range
{uniqueAgeGroups.map(age => (
{age}
))}
All
)}
{isLoading ? (
<>
Searching...
>
) : (
hasSearched && !hasFiltersChanged() ? 'Search' : hasFiltersChanged() ? 'Update Search' : 'Search'
)}
{hasSearched && !isLoading && (
)}
{!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 ? (
) : (
<>
{(() => {
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.
setDisplayCount(prev => Math.min(prev + groupsPerPage, totalGroups))}
>
Show More
)}
{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
Show Groups at All 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()} > : ''}
{ setSelectedGroupForJoin(null); setJoinModalType('waitlist'); setShowJoinModal(true); }}
style={{
padding: '0.75rem 2rem',
background: '#0f4c5c',
color: 'white',
border: 'none',
borderRadius: '8px',
fontSize: '0.95rem',
fontWeight: '600',
cursor: 'pointer'
}}
>
Join Waitlist
>
) : (
<>
{/* 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
{ setSelectedGroupForJoin(null); setJoinModalType('waitlist-anytime'); setShowJoinModal(true); }}
style={{
padding: '0.75rem 2rem',
background: '#0f4c5c',
color: 'white',
border: 'none',
borderRadius: '8px',
fontSize: '0.95rem',
fontWeight: '600',
cursor: 'pointer'
}}
>
Join Waitlist
{/* 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()} > : ''}
{ setSelectedGroupForJoin(null); setJoinModalType('waitlist'); setShowJoinModal(true); }}
style={{
padding: '0.75rem 2rem',
background: '#0f4c5c',
color: 'white',
border: 'none',
borderRadius: '8px',
fontSize: '0.95rem',
fontWeight: '600',
cursor: 'pointer'
}}
>
Join Waitlist
>
)}
>
)}
)}
>
);
})()}
>
)}
{/* 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.
>
)}
setJoinFlowStep('login')}
>
Sign In / Create Account
{ setShowJoinModal(false); setJoinFlowStep('initial'); }}
>
Cancel
)}
{joinFlowStep === 'login' && (
Please login with your existing account
Sign in
setJoinFlowStep('initial')}
>
← Back
)}
{joinFlowStep === 'confirmation' && (
{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
Remove from Waitlist
>
) : (
<>
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'}
{
setJoinFlowStep('removed');
}}>
Remove from Waitlist
>
)}
Not sure? Browse through other times and groups, you won't be removed from your {joinModalType === 'join' ? 'group' : 'waitlist'}.
{
setShowJoinModal(false);
setJoinFlowStep('initial');
setSelectedGroupForJoin(null);
handleStartOver();
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
>
Find a Different Group
)}
{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.
{
setShowJoinModal(false);
setJoinFlowStep('initial');
setSelectedGroupForJoin(null);
handleStartOver();
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
>
Find a Different Group
)}
>
)}
{/* Search Mode Confirmation Modal */}
{/* Fullscreen Language Picker (Mobile) */}
{showLanguageFullscreen && (
{
setShowLanguageFullscreen(false);
setLanguageSearchTerm('');
}}>
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?
Search by Name Only
Keep My Filters
setShowSearchModeModal(false)}
>
Cancel
)}
{showTimezoneModal && (
setShowTimezoneModal(false)}>
e.stopPropagation()}>
Select Time Zone
setShowTimezoneModal(false)}
>
×
Time Zone:
{getTimezoneInfo(pendingTimeZone).zone}
Closest City:
setPendingTimeZone(e.target.value)}
style={{
padding: '0.75rem',
fontSize: '1rem',
border: '2px solid #e9ecef',
borderRadius: '8px',
fontFamily: 'Roboto, sans-serif',
width: '100%',
cursor: 'pointer'
}}
>
Los Angeles, CA - United States (GMT-8)
Denver, CO - United States (GMT-7)
Chicago, IL - United States (GMT-6)
New York, NY - United States (GMT-5)
São Paulo - Brazil (GMT-3)
Mexico City - Mexico (GMT-6)
London - United Kingdom (GMT+0)
Paris - France (GMT+1)
Berlin - Germany (GMT+1)
Moscow - Russia (GMT+3)
Casablanca - Morocco (GMT+0)
Cairo - Egypt (GMT+2)
Lagos - Nigeria (GMT+1)
Johannesburg - South Africa (GMT+2)
Dubai - UAE (GMT+4)
Mumbai - India (GMT+5:30)
Hong Kong (GMT+8)
Singapore (GMT+8)
Tokyo - Japan (GMT+9)
Seoul - South Korea (GMT+9)
Sydney - Australia (GMT+10)
Auckland - New Zealand (GMT+12)
{
setPendingTimeZone(timeZone); // Reset to current
setShowTimezoneModal(false);
}}
>
Cancel
{
setTimeZone(pendingTimeZone); // Apply the pending timezone
setShowTimezoneModal(false);
}}
>
Apply
)}
{/* Sort/Filter Modal */}
{showSortModal && (
setShowSortModal(false)}>
e.stopPropagation()}>
Sort / Filter
setShowSortModal(false)}
>
×
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 && (
setSelectedAgeGroupsFilter([])}
>
Clear Age Filter
)}
setShowSortModal(false)}
>
Cancel
setShowSortModal(false)}
>
Apply
)}
);
};
// Component ready for use - rendered by HTML file