UNPKG

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
"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