Skip to content

TimelineList gray bottom part. #2720

@valavanisleonidas

Description

@valavanisleonidas

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.

Image

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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions