-
Couldn't load subscription status.
- Fork 3.1k
Open
Description
Hello,
I am using the latest version "react-native-calendars": "^1.1313.0" and react native "0.80.0". I have a screen that uses timelinelist and inputs some filters that loads some data from an api. When i load the data/events into the calendar there is a gray part in the bottom .
any ideas on the issue? thanks a lot.
code and image shared below.
code for the screen is
// ---- Config ----
const INITIAL_TIME = { hour: 9, minutes: 0 };
const MAX_WEEKS_CACHE = 8;
const keyFromArray = (arr?: Array<string | number> | null) =>
Array.isArray(arr) && arr.length ? [...arr].map(String).sort().join(',') : 'none';
type EventData = { caregiverIds: number[]; startISO: string; endISO: string };
const getInitials = (full?: string) => {
const parts = (full || '').trim().split(/\s+/).filter(Boolean);
if (!parts.length) return '';
if (parts.length === 1) return (parts[0][0] || '').toUpperCase();
return ((parts[0][0] || '') + (parts[1][0] || '')).toUpperCase();
};
// do it outside to be sure its rendered before the calendar
const lang = useThemeStore.getState().language;
setCalendarLocale(lang === 'gr' ? 'gr' : 'en');
export default function AvailabilityScreen() {
const [currentDate, setCurrentDate] = useState<string>(getDate());
const [eventsByDate, setEventsByDate] = useState<Record<string, TimelineEventProps[]>>({});
const [isLoading, setIsLoading] = useState(false);
const [serviceIds, setServiceIds] = useState<Array<number | string>>([]);
const [caregiverFilterIds, setCaregiverFilterIds] = useState<number[]>([]);
const [durationMinutes, setDurationMinutes] = useState<number | null>(null);
const [shouldFetch, setShouldFetch] = useState(false);
const weekCacheRef = useRef<Map<string, Record<string, TimelineEventProps[]>>>(new Map());
useEffect(() => {
(async () => {
try {
const all = await getAllCaregivers();
for (const caregiver of all) {
const id = caregiver.caregiverId;
const label = [caregiver.firstName, caregiver.lastName].filter(Boolean).join(' ').trim() || `#${id}`;
caregiversByIdRef.current.set(id, label);
}
} catch (e) {
console.warn('Failed to load caregivers:', e);
}
})();
}, []);
const baseTimelineProps: Partial<TimelineProps> = useMemo(
() => ({
format24h: true,
unavailableHours: [
{ start: 0, end: 6 },
{ start: 20, end: 24 },
],
overlapEventsSpacing: 8,
rightEdgeSpacing: 24,
}),
[],
);
const cacheKey = useMemo(() => {
const wk = weekKey(currentDate);
const d = durationMinutes ?? 'none';
const s = keyFromArray(serviceIds);
const c = keyFromArray(caregiverFilterIds);
return `${wk}|d:${d}|s:${s}|c:${c}`;
}, [currentDate, durationMinutes, serviceIds, caregiverFilterIds]);
const hasEvents = useMemo(
() => Object.keys(eventsByDate).some((k) => (eventsByDate[k] ?? []).length > 0),
[eventsByDate],
);
const safeEventsByDate = useMemo(
() => ({ ...eventsByDate, [currentDate]: eventsByDate[currentDate] ?? [] }),
[eventsByDate, currentDate],
);
const markedDates = useMemo(() => {
const marked: Record<string, { marked: boolean }> = {};
Object.entries(eventsByDate).forEach(([date, list]) => {
if ((list ?? []).length > 0) marked[date] = { marked: true };
});
return marked;
}, [eventsByDate]);
const caregiversByIdRef = useRef<Map<number, string>>(new Map());
const mapApiToTimeline = useCallback((rows: CaregiverAvailabilityResponse): TimelineEventProps[] => {
type Entry = { startISO: string; endISO: string; caregivers: Set<number> };
const slotMap = new Map<string, Entry>();
rows.forEach((row: any) => {
const cid = row.caregiverId;
if (cid && !caregiversByIdRef.current.has(cid)) {
caregiversByIdRef.current.set(cid, `#${cid}`);
}
(row.availability || []).forEach((day: any) => {
(day.slots || []).forEach((slot: any) => {
const key = `${slot.start}|${slot.end}`;
let entry = slotMap.get(key);
if (!entry) {
entry = { startISO: slot.start, endISO: slot.end, caregivers: new Set<number>() };
slotMap.set(key, entry);
}
entry.caregivers.add(cid);
});
});
});
const events: TimelineEventProps[] = [];
for (const [key, entry] of slotMap.entries()) {
const startStr = formatStringDateTime(new Date(entry.startISO));
const endStr = formatStringDateTime(new Date(entry.endISO));
const ids = Array.from(entry.caregivers).sort((a, b) => a - b);
const initials = ids.map((id) => getInitials(caregiversByIdRef.current.get(id) || `#${id}`)).slice(0, 3);
const title =
ids.length === 1
? strings.availableCaregivers_one || '1 available caregiver'
: (strings.availableCaregivers_other || '{count} available caregivers').replace(
'{count}',
String(ids.length),
);
events.push({
id: `${key}|${ids.join('-')}`,
start: startStr,
end: endStr,
title: `${title} • (${initials.join(', ')}${ids.length > 3 ? '…' : ''})`,
color: 'lightgreen',
data: { caregiverIds: ids, startISO: entry.startISO, endISO: entry.endISO } as EventData,
} as TimelineEventProps);
}
return events;
}, []);
useEffect(() => {
if (!shouldFetch) return;
if (durationMinutes == null) return;
if (weekCacheRef.current.has(cacheKey)) {
setEventsByDate(weekCacheRef.current.get(cacheKey)!);
return;
}
const weekDates = getWeekDates(currentDate);
setIsLoading(true);
(async () => {
try {
const payload: any = {
dates: weekDates,
durationMinutes: durationMinutes as number,
};
if (serviceIds && serviceIds.length) payload.serviceIds = serviceIds.map((x) => Number(x));
if (caregiverFilterIds && caregiverFilterIds.length) payload.caregiverIds = caregiverFilterIds;
const res = await getCaregiversAvailabilityByDates(payload);
const evs = mapApiToTimeline(res as CaregiverAvailabilityResponse);
const grouped = groupBy(evs, (e) => e.start.slice(0, 10)) as Record<string, TimelineEventProps[]>;
weekCacheRef.current.set(cacheKey, grouped);
if (weekCacheRef.current.size > MAX_WEEKS_CACHE) {
const firstKey = weekCacheRef.current.keys().next().value;
if (firstKey !== undefined) {
weekCacheRef.current.delete(firstKey);
}
}
setEventsByDate(grouped);
} catch (e) {
console.warn('Failed to fetch availability:', e);
} finally {
setIsLoading(false);
}
})();
}, [shouldFetch, cacheKey, currentDate, durationMinutes, serviceIds, caregiverFilterIds, mapApiToTimeline]);
const [keyboardVisible, setKeyboardVisible] = useState(false);
useEffect(() => {
const show = Keyboard.addListener('keyboardDidShow', () => setKeyboardVisible(true));
const hide = Keyboard.addListener('keyboardDidHide', () => setKeyboardVisible(false));
return () => {
show.remove();
hide.remove();
};
}, []);
// -------- Modal state ----------
const [modalVisible, setModalVisible] = useState(false);
const [modalCaregiverIds, setModalCaregiverIds] = useState<number[]>([]);
const handleEventPress = useCallback((evt: TimelineEventProps) => {
const data = (evt as any)?.data as EventData | undefined;
const ids = data?.caregiverIds ?? [];
setModalCaregiverIds(ids);
setModalVisible(true);
}, []);
const filterRef = useRef<any>(null);
useFocusEffect(
useCallback(() => {
// Reset filters
filterRef.current?.resetFilters?.();
// Reset calendar-related state
// setEventsByDate({});
// setServiceIds([]);
// setCaregiverFilterIds([]);
// setDurationMinutes(null);
// setShouldFetch(false);
// weekCacheRef.current.clear();
// Reset current date to today
// setCurrentDate(getDate());
}, []),
);
return (
<SafeAreaView style={styles.flex1}>
{/* IMPORTANT: useScroll={false} so no ScrollView wraps the list */}
<KeyBoardAvoidWrapper useScroll={false} verticalOffset={90}>
<FilterHeader
ref={filterRef}
onApply={({ serviceIds: sIds, caregiverIds: cIds, duration }) => {
Keyboard.dismiss();
setServiceIds(Array.isArray(sIds) ? sIds : sIds ? [sIds] : []);
const idsOnly = Array.isArray(cIds) ? cIds : cIds ? [cIds] : [];
setCaregiverFilterIds(idsOnly.map((id) => Number(id)));
const parsedDuration =
duration && !isNaN(Number(duration)) ? Math.max(1, Math.floor(Number(duration))) : null;
setDurationMinutes(parsedDuration);
// Reset
weekCacheRef.current.clear();
setEventsByDate({ [currentDate]: [] });
setShouldFetch(true);
}}
/>
<View style={styles.calendarArea}>
<CalendarProvider
date={currentDate}
onDateChanged={setCurrentDate}
onMonthChange={() => {}}
showTodayButton
disabledOpacity={0.6}
>
<ExpandableCalendar
firstDay={1}
leftArrowImageSource={require('../assets/images/previous.png')}
rightArrowImageSource={require('../assets/images/next.png')}
markedDates={markedDates}
/>
{isLoading && (
<View style={styles.loaderWrap}>
<ActivityIndicator />
</View>
)}
<TimelineList
key={`${cacheKey}|kb:${keyboardVisible}`}
events={safeEventsByDate}
showNowIndicator
scrollToNow={hasEvents}
scrollToFirst={hasEvents}
initialTime={INITIAL_TIME}
timelineProps={{
...baseTimelineProps,
onEventPress: handleEventPress,
}}
/>
</CalendarProvider>
</View>
</KeyBoardAvoidWrapper>
{/* Caregivers modal */}
<Modal visible={modalVisible} transparent animationType="slide" onRequestClose={() => setModalVisible(false)}>
<View style={styles.modalBackdrop}>
<View style={styles.modalCard}>
<Text style={styles.modalTitle}>{strings.caregiversListTitle}</Text>
<FlatList
data={modalCaregiverIds}
keyExtractor={(id) => String(id)}
renderItem={({ item: id }) => (
<Text style={styles.modalItem}>{caregiversByIdRef.current.get(id) || `#${id}`}</Text>
)}
ItemSeparatorComponent={FlatListSeparator}
style={{ maxHeight: 260 }}
/>
<TouchableOpacity style={styles.modalCloseBtn} onPress={() => setModalVisible(false)}>
<Text style={styles.modalCloseTxt}>{strings.close}</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
</SafeAreaView>
);
}
const FlatListSeparator = () => <View style={{ height: 8 }} />;
const styles = StyleSheet.create({
flex1: { flex: 1 },
loaderWrap: { paddingVertical: 8 },
calendarArea: { flex: 1 },
modalBackdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.32)',
justifyContent: 'center',
alignItems: 'center',
padding: 24,
},
modalCard: {
width: '100%',
borderRadius: 12,
backgroundColor: '#fff',
padding: 16,
},
modalTitle: { fontSize: 18, fontWeight: '600', marginBottom: 12 },
modalItem: { fontSize: 16 },
modalCloseBtn: {
marginTop: 16,
alignSelf: 'flex-end',
paddingVertical: 8,
paddingHorizontal: 14,
borderRadius: 8,
backgroundColor: '#0a84ff',
},
modalCloseTxt: { color: '#fff', fontWeight: '600' },
});
Metadata
Metadata
Assignees
Labels
No labels