react-native-timeline-view
Version:
react-native-timeline-view is a fully customizable timeline component for React Native that renders time slots with smart booking display. It highlights ongoing meetings with real-time indicators, supports multi-slot bookings, and displays available/unava
267 lines (263 loc) • 9.25 kB
JavaScript
"use strict";
import React, { useState, useEffect } from 'react';
import { View, Text, ScrollView, StyleSheet, Dimensions, TouchableOpacity } from 'react-native';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const {
width: initialWidth,
height: initialHeight
} = Dimensions.get('window');
const TimeLineView = ({
slots = [],
onPress,
dynamicStyle,
stylesConfig = {},
autoRefresh = false,
pollingInterval = 2000,
fetchSlots,
startTime,
slotDuration = 15,
labelEverySlot = 2,
roundToNearestSlot = true,
// Default true
renderBookingContent
}) => {
const [landscape] = useState(initialWidth > initialHeight);
const [generatedHours, setGeneratedHours] = useState(() => generateHours(slots, startTime, slotDuration, roundToNearestSlot));
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
const timeInterval = setInterval(() => {
setCurrentTime(new Date());
}, 1000); // Update every second
return () => clearInterval(timeInterval);
}, []);
useEffect(() => {
setGeneratedHours(generateHours(slots, startTime, slotDuration, roundToNearestSlot));
}, [slots, startTime, slotDuration, roundToNearestSlot]);
useEffect(() => {
if (autoRefresh && typeof fetchSlots === 'function') {
const interval = setInterval(async () => {
const updatedSlots = await fetchSlots();
setGeneratedHours(generateHours(updatedSlots, startTime, slotDuration, roundToNearestSlot));
}, pollingInterval);
return () => clearInterval(interval);
}
return undefined;
}, [autoRefresh, pollingInterval, fetchSlots, startTime, slotDuration, roundToNearestSlot]);
const renderTimeSlot = (hourObj, index) => {
const hour = hourObj.time;
const now = currentTime;
const slotStart = new Date(hourObj.isoTime);
const slotEnd = new Date(slotStart);
slotEnd.setMinutes(slotEnd.getMinutes() + slotDuration);
const isCurrentTimeSlot = now >= slotStart && now < slotEnd;
const getSlotHeight = () => {
const defaultHeight = styles.hourContainer.height || 60;
const customHourContainer = stylesConfig.hourContainer ? StyleSheet.flatten(stylesConfig.hourContainer) : undefined;
const customHeight = customHourContainer && typeof customHourContainer.height === 'number' ? customHourContainer.height : undefined;
return customHeight || defaultHeight;
};
const currentSlotHeight = getSlotHeight();
const slotHeightPerMinute = currentSlotHeight / slotDuration;
const calculateCurrentTimeOffset = () => {
if (!isCurrentTimeSlot) return 0;
const elapsed = now.getTime() - slotStart.getTime();
const total = slotEnd.getTime() - slotStart.getTime();
const percent = elapsed / total;
return percent * currentSlotHeight;
};
const currentTimeOffset = calculateCurrentTimeOffset();
const calculateBookingOffsetAndHeight = () => {
if (!hourObj.Event || hourObj.available) return {
height: currentSlotHeight,
top: 0
};
const EventStart = new Date(hourObj.Event.startDate);
const EventEnd = new Date(hourObj.Event.endDate);
const overlapStart = new Date(Math.max(EventStart.getTime(), slotStart.getTime()));
const overlapEnd = new Date(Math.min(EventEnd.getTime(), slotEnd.getTime()));
const overlapDuration = Math.max(0, (overlapEnd.getTime() - overlapStart.getTime()) / (1000 * 60));
const offsetMinutes = Math.max(0, (overlapStart.getTime() - slotStart.getTime()) / (1000 * 60));
const offsetTop = offsetMinutes * slotHeightPerMinute;
const height = overlapDuration * slotHeightPerMinute;
return {
height,
top: offsetTop
};
};
const {
height: EventHeight,
top: EventOffsetTop
} = calculateBookingOffsetAndHeight();
return /*#__PURE__*/_jsxs(TouchableOpacity, {
onPress: () => onPress?.(hourObj),
style: [styles.hourContainer, stylesConfig.hourContainer],
children: [!hourObj.available && /*#__PURE__*/_jsx(View, {
style: [styles.unavailableSlot, stylesConfig.unavailableSlot, {
height: EventHeight,
top: EventOffsetTop,
position: 'absolute' // important for correct offset
}],
children: renderBookingContent ? renderBookingContent(hourObj.Event, index) : /*#__PURE__*/_jsx(Text, {
style: [styles.EventTitle, stylesConfig.EventTitle],
children: hourObj?.Event?.title || 'Reserved'
})
}), isCurrentTimeSlot && /*#__PURE__*/_jsxs(View, {
style: [styles.currentLine, stylesConfig.currentLine, {
top: currentTimeOffset,
transform: [{
translateY: -1
}]
}],
children: [/*#__PURE__*/_jsx(View, {
style: [styles.currentDot, stylesConfig.currentDot]
}), /*#__PURE__*/_jsx(Text, {
style: [styles.hourText, stylesConfig.hourText, {
position: 'absolute',
right: -70
}],
children: formatTime(now)
})]
}), /*#__PURE__*/_jsx(View, {
style: [styles.line, stylesConfig.line, !hourObj?.available && {
position: 'absolute',
zIndex: -1
}]
}), index % labelEverySlot === 0 && /*#__PURE__*/_jsx(Text, {
style: [styles.hourText, stylesConfig.hourText],
children: hour
})]
}, index);
};
return /*#__PURE__*/_jsx(ScrollView, {
style: [styles.scrollContainer, !landscape ? {
height: 350,
width: 650
} : {}, dynamicStyle, stylesConfig.scrollContainer],
showsVerticalScrollIndicator: true,
persistentScrollbar: true,
children: /*#__PURE__*/_jsx(View, {
style: [styles.container, stylesConfig.container],
children: generatedHours.map((hour, index) => /*#__PURE__*/_jsx(View, {
style: [styles.slotContainer, stylesConfig.slotContainer],
children: renderTimeSlot(hour, index)
}, index))
})
});
};
const generateHours = (slots, startTime, slotDuration = 15, roundToNearestSlot = true) => {
const hours = [];
const minElements = 20;
const intervalMinutes = slotDuration;
let currentTime = startTime ? new Date(startTime) : slots[0]?.slot ? new Date(slots[0].slot) : new Date();
// Round to nearest slot interval only if user wants it
if (roundToNearestSlot) {
currentTime.setMinutes(Math.floor(currentTime.getMinutes() / intervalMinutes) * intervalMinutes, 0, 0);
} else {
// Keep exact time but reset seconds and ms
currentTime.setSeconds(0, 0);
}
const Events = slots.filter(slot => slot.Event).map(slot => slot.Event);
while (hours.length < minElements) {
const slotStart = new Date(currentTime);
const slotEnd = new Date(slotStart);
slotEnd.setMinutes(slotEnd.getMinutes() + intervalMinutes);
// Check for overlapping Events based on actual Event times
const overlappingBooking = Events.find(b => {
const start = new Date(b.startDate);
const end = new Date(b.endDate);
return start < slotEnd && end > slotStart // Any overlap
;
});
const matchingSlot = slots.find(slot => new Date(slot.slot).toISOString() === slotStart.toISOString());
const hour = formatTime(slotStart);
hours.push({
time: hour,
available: overlappingBooking ? false : matchingSlot?.available !== false,
Event: overlappingBooking || {},
isoTime: slotStart.toISOString()
});
currentTime.setMinutes(currentTime.getMinutes() + intervalMinutes);
}
return hours;
};
const formatTime = date => {
let hours = date.getHours();
const minutes = date.getMinutes();
const period = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12 || 12;
const paddedMinutes = minutes === 0 ? '00' : minutes.toString().padStart(2, '0');
return `${hours.toString().padStart(2, '0')}:${paddedMinutes} ${period}`;
};
const styles = StyleSheet.create({
scrollContainer: {
flexGrow: 1,
height: 500,
width: 200,
paddingTop: 20
},
container: {
alignItems: 'center',
position: 'relative'
},
hourContainer: {
height: 60,
justifyContent: 'space-between',
alignItems: 'flex-start',
width: '100%',
flexDirection: 'row'
},
line: {
height: 1,
backgroundColor: 'black',
width: '80%',
opacity: 0.2
},
hourText: {
position: 'absolute',
right: 0,
top: -10,
color: 'black',
fontSize: 15,
fontWeight: '500',
opacity: 0.5
},
currentLine: {
position: 'absolute',
top: '50%',
left: 0,
width: '83%',
height: 2,
backgroundColor: '#176CFF',
opacity: 0.8
},
currentDot: {
position: 'absolute',
right: 0,
top: -6,
width: 12,
height: 12,
borderRadius: 6,
backgroundColor: '#176CFF'
},
unavailableSlot: {
backgroundColor: '#F2F2F2',
width: '75%',
height: 60,
justifyContent: 'center',
alignItems: 'flex-start',
borderRadius: 5,
paddingHorizontal: 10
},
EventTitle: {
color: '#000',
fontSize: 14,
fontWeight: '600'
},
slotContainer: {
position: 'relative',
width: '100%',
height: 60
}
});
export default TimeLineView;
//# sourceMappingURL=TimeLineView.js.map