TimelineList gray bottom part.
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' },
});
Hey we are also facing something similar, did you resolve this somehow?
Hey we are also facing something similar, did you resolve this somehow?
not really. i havent figured it out yet.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.