import React, {useMemo, useRef} from "react"; import {useWindowDimensions, View, Animated} from "react-native"; import NowLine from "./components/NowLine"; import {clusterizer, prepareTimetable, setClusterWidth, setNodesPosition} from "./helpers/eventsPreparer"; import {dateRangesOverlap, daysDiff, minDiff, normalizeTime} from "./helpers/date"; import {CardProps, Day, TimetableProps} from "./types"; import Headers from "./components/Headers"; import Hours from "./components/Hours"; function withDefault(value: A, defaultValue: NonNullable) { return typeof value === "undefined" ? defaultValue : value as NonNullable; } function Timetable({ items, renderItem, date, range: rangeProp, fromHour = 0, toHour = 24, style, width, timeWidth = 50, itemMinHeightInMinutes = 25, hourHeight = 60, linesTopOffset = 18, linesLeftInset = 15, columnHorizontalPadding = 10, stickyHours, renderHeader, renderHour, startProperty = 'startDate' as keyof I, endProperty = 'endDate' as keyof I, ...props }: TimetableProps) { const screenWidth = useWindowDimensions().width; const range = { from: normalizeTime(date || rangeProp?.from), till: normalizeTime(date || rangeProp?.till, 23, 59, 59, 999), }; const columnDays = React.useMemo(() => { const amountOfDays = daysDiff(range.till, range.from) + 1; const days = []; for (let i = 0; i < amountOfDays; i++) { const date = new Date(range.from); date.setDate(date.getDate() + i); const start = new Date(date); start.setHours(fromHour, 0, 0, 0); const end = new Date(date); end.setHours(toHour - 1, 59, 59, 999); days.push({date, start, end}); } return days; }, [range.from, range.till, fromHour, toHour]); const scrollX = useRef(new Animated.Value(0)).current; const linesLeftOffset = timeWidth - linesLeftInset; const minuteHeight = hourHeight / 60; const columnWidth = withDefault(props.columnWidth, (width || screenWidth) - (timeWidth - linesLeftInset)); const calculateTopOffset = (date: number) => { const d = new Date(date); return (Math.max((d.getHours() - fromHour), 0) * 60 + d.getMinutes()) * minuteHeight + linesTopOffset; }; const cards = useMemo(() => { if (!Array.isArray(items)) return []; const positionedEvents: CardProps[] = []; const itemMinHeight = Math.max(itemMinHeightInMinutes, 25); columnDays.forEach((columnDay, columnIndex) => { // Filter event by column date const filteredItems = items.filter(item => dateRangesOverlap( columnDay.start, columnDay.end, new Date(item[startProperty] as any), new Date(item[endProperty] as any) )); // If length === 0 skip process if (!filteredItems?.length) return; const { preparedEvents, minutes } = prepareTimetable(filteredItems, startProperty, endProperty, itemMinHeight, columnDay); const clusteredTimetable = clusterizer(preparedEvents, minutes); setClusterWidth(clusteredTimetable, columnWidth); setNodesPosition(clusteredTimetable); for (let nodeId in clusteredTimetable.nodes) { let node = clusteredTimetable.nodes[nodeId]; if (!node.cluster || node.position === null) continue; let data = node?.data; const itemStart = new Date(data[startProperty] as any); const itemEnd = new Date(data[endProperty] as any); const itemMinEnd = new Date(itemStart); itemMinEnd.setMinutes(itemStart.getMinutes() + itemMinHeight); const daysTotal = daysDiff(+itemStart, +itemEnd) + 1; const neighboursCount = Object.keys(node?.neighbours).length; // card begins either at column's beginning or item's start time, whatever is greater const start = Math.max(+columnDay.start, +itemStart); // card ends either at column's end or item's end time, whatever is lesser const end = Math.min(+columnDay.end + 1, Math.max(+itemEnd, +itemMinEnd)); let width = neighboursCount > 0 ? columnIndex > 0 ? node.cluster.width - (columnHorizontalPadding) : node.cluster.width : node.cluster.width - (columnHorizontalPadding * 2); let left = (linesLeftOffset + columnIndex * columnWidth + columnHorizontalPadding) + (node.cluster.width * node.position); if (neighboursCount > 0 && node?.isLast) { width = node.cluster.width - (columnHorizontalPadding * 2); } if (columnIndex === 0) { width = width - linesLeftInset; left = left + linesLeftInset; } positionedEvents.push({ key: columnIndex + node.key, item: node.data, daysTotal, style: { position: 'absolute', zIndex: 2, top: calculateTopOffset(start), left, height: minDiff(start, end) * minuteHeight, width }, }); } }); return positionedEvents; }, [ items, columnDays, startProperty, endProperty, columnWidth, columnHorizontalPadding, linesLeftOffset, linesLeftInset, minuteHeight, ]); return ( {!props.hideNowLine && ( )} {/* Cards */} {!!renderItem && cards.map(renderItem)} ); } export default Timetable;