@konnorkooi/schedule-glance
Version:
A customizable weekly schedule component
716 lines (682 loc) • 31.6 kB
JavaScript
require("./index.css");
var $iEn1Z$reactjsxruntime = require("react/jsx-runtime");
var $iEn1Z$react = require("react");
var $iEn1Z$html2canvas = require("html2canvas");
function $parcel$exportWildcard(dest, source) {
Object.keys(source).forEach(function(key) {
if (key === 'default' || key === '__esModule' || Object.prototype.hasOwnProperty.call(dest, key)) {
return;
}
Object.defineProperty(dest, key, {
enumerable: true,
get: function get() {
return source[key];
}
});
});
return dest;
}
function $parcel$export(e, n, v, s) {
Object.defineProperty(e, n, {get: v, set: s, enumerable: true, configurable: true});
}
function $parcel$interopDefault(a) {
return a && a.__esModule ? a.default : a;
}
$parcel$export(module.exports, "Schedule", () => $a93b004b8c50d707$export$2e2bcd8739ae039);
$parcel$export(module.exports, "ScheduleHeader", () => $9d173187df8f14a9$export$2e2bcd8739ae039);
$parcel$export(module.exports, "ScheduleCell", () => $32b595b0729c05b4$export$2e2bcd8739ae039);
$parcel$export(module.exports, "EventPopup", () => $2c98468b9b21b1f5$export$2e2bcd8739ae039);
const $5ae523ed11da3f14$export$d425309ca6befbaf = (events)=>{
return events.sort((a, b)=>{
// Convert time strings to comparable numbers
const aTime = $5ae523ed11da3f14$export$6cd6be75c9d108e9(a.start);
const bTime = $5ae523ed11da3f14$export$6cd6be75c9d108e9(b.start);
return aTime - bTime;
});
};
const $5ae523ed11da3f14$export$6cd6be75c9d108e9 = (time)=>{
const [hours, minutes] = time.split(":").map(Number);
return hours * 60 + minutes;
};
const $5ae523ed11da3f14$export$9a827e9be85de527 = (minutes)=>{
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours.toString().padStart(2, "0")}:${mins.toString().padStart(2, "0")}`;
};
const $5ae523ed11da3f14$export$51bd2919d2d38e79 = (events)=>{
if (!events.length) return {
startHour: 9,
endHour: 13
}; // Default business hours if no events
let earliestStart = 1440; // Initialize to end of day
let latestEnd = 0;
events.forEach((event)=>{
const startMinutes = $5ae523ed11da3f14$export$6cd6be75c9d108e9(event.start);
const endMinutes = $5ae523ed11da3f14$export$6cd6be75c9d108e9(event.end);
if (startMinutes < earliestStart) earliestStart = startMinutes;
if (endMinutes > latestEnd) latestEnd = endMinutes;
});
// Round down to the nearest hour for start time
const startHour = Math.max(0, Math.floor(earliestStart / 60));
// Round up to the next hour for end time and add 1 hour padding
const endHour = Math.min(24, Math.ceil(latestEnd / 60));
// Ensure there's always at least 3 hours shown
if (endHour - startHour < 3) return {
startHour: Math.max(0, startHour - 1),
endHour: Math.min(24, endHour + 1)
};
return {
startHour: startHour,
endHour: endHour
};
};
const $2c98468b9b21b1f5$var$EventPopup = ({ event: event, onClose: onClose })=>{
return /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("div", {
className: "event-popup-overlay",
style: {
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 0,
padding: 0,
width: "100vw",
height: "100vh",
backgroundColor: "rgba(0, 0, 0, 0.5)",
zIndex: 9999,
display: "flex",
alignItems: "center",
justifyContent: "center",
pointerEvents: "auto"
},
onClick: onClose,
children: [
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("div", {
className: "event-popup",
style: {
backgroundColor: "white",
padding: "20px",
borderRadius: "10px",
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08)",
maxWidth: "400px",
width: "90%",
position: "relative",
maxHeight: "90vh",
overflow: "auto",
animation: "popupFadeIn 0.3s ease-out",
transform: "none",
margin: 0,
left: "auto",
right: "auto",
top: "auto",
bottom: "auto" // Remove any bottom positioning
},
onClick: (e)=>e.stopPropagation(),
children: [
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("div", {
style: {
marginBottom: "20px"
},
children: [
event.title && /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("h3", {
style: {
margin: 0,
fontSize: "18px",
fontWeight: "bold",
marginBottom: "10px",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
color: "#333"
},
children: event.title
}),
event.body && /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("p", {
style: {
margin: "2px 0",
fontSize: "14px",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
color: "#666",
lineHeight: "1.5"
},
children: event.body
}),
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("div", {
style: {
fontSize: "14px",
color: "#666",
marginTop: "10px",
padding: "8px",
backgroundColor: "#f5f5f5",
borderRadius: "5px",
display: "inline-block"
},
children: [
event.start,
" - ",
event.end
]
})
]
}),
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("button", {
onClick: onClose,
style: {
backgroundColor: "#007bff",
color: "white",
border: "none",
padding: "10px 20px",
borderRadius: "5px",
cursor: "pointer",
transition: "all 0.2s ease-in-out",
width: "100%",
fontSize: "14px",
fontWeight: "500"
},
onMouseOver: (e)=>e.currentTarget.style.backgroundColor = "#0056b3",
onMouseOut: (e)=>e.currentTarget.style.backgroundColor = "#007bff",
children: "Close"
})
]
}),
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("style", {
children: `
popupFadeIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.event-popup-overlay {
position: fixed !important;
left: 0 !important;
top: 0 !important;
width: 100vw !important;
height: 100vh !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
`
})
]
});
};
var $2c98468b9b21b1f5$export$2e2bcd8739ae039 = $2c98468b9b21b1f5$var$EventPopup;
const $a93b004b8c50d707$var$Schedule = /*#__PURE__*/ (0, $iEn1Z$react.forwardRef)(({ events: initialEvents, onEventClick: onEventClick, headers: headers = [
{
label: "Monday",
dayIndex: 0
},
{
label: "Tuesday",
dayIndex: 1
},
{
label: "Wednesday",
dayIndex: 2
},
{
label: "Thursday",
dayIndex: 3
},
{
label: "Friday",
dayIndex: 4
}
], customPopupHandler: customPopupHandler, useDefaultPopup: useDefaultPopup = true, emptyStateMessage: emptyStateMessage = "No events scheduled" }, ref)=>{
const [events, setEvents] = (0, $iEn1Z$react.useState)(initialEvents);
const [selectedEvent, setSelectedEvent] = (0, $iEn1Z$react.useState)(null);
const [startHour, setStartHour] = (0, $iEn1Z$react.useState)(8);
const [endHour, setEndHour] = (0, $iEn1Z$react.useState)(18);
const scheduleRef = (0, $iEn1Z$react.useRef)(null);
const [isExporting, setIsExporting] = (0, $iEn1Z$react.useState)(false);
const [windowWidth, setWindowWidth] = (0, $iEn1Z$react.useState)(window.innerWidth);
(0, $iEn1Z$react.useEffect)(()=>{
const handleResize = ()=>setWindowWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return ()=>window.removeEventListener("resize", handleResize);
}, []);
(0, $iEn1Z$react.useEffect)(()=>{
setEvents(initialEvents);
// Add padding to start and end hours to prevent tight fit
const { startHour: newStartHour, endHour: newEndHour } = (0, $5ae523ed11da3f14$export$51bd2919d2d38e79)(initialEvents);
setStartHour(Math.max(0, newStartHour)); // could Add 1 hour padding at start -1
setEndHour(Math.min(24, newEndHour)); // could Add 1 hour padding at end +1
}, [
initialEvents
]);
// Expose the export function via ref
(0, $iEn1Z$react.useImperativeHandle)(ref, ()=>({
exportToPng: async (filename = "schedule-export.png")=>{
if (!scheduleRef.current) {
console.error("Schedule ref is null");
return;
}
setIsExporting(true);
try {
console.log("Starting export...");
const canvas = await (0, ($parcel$interopDefault($iEn1Z$html2canvas)))(scheduleRef.current, {
backgroundColor: "#ffffff",
scale: 2,
logging: true,
useCORS: true,
allowTaint: true
});
console.log("Canvas generated, creating download...");
canvas.toBlob((blob)=>{
if (blob) {
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
console.log("Download initiated");
} else console.error("Failed to create blob");
}, "image/png", 1.0);
} catch (error) {
console.error("Failed to export PNG:", error);
throw error; // Re-throw to allow error handling by the consumer
} finally{
setIsExporting(false);
}
}
}));
const handleEventClick = (event)=>{
if (customPopupHandler) customPopupHandler(event);
else if (useDefaultPopup) setSelectedEvent(event);
if (onEventClick) onEventClick(event);
};
const formatHour = (hour)=>{
const period = hour >= 12 ? "PM" : "AM";
const displayHour = hour % 12 || 12;
return `${displayHour}:00 ${period}`;
};
// Rest of the existing renderEventContent function remains the same
// In src/components/Schedule.tsx
// Update the renderEventContent function:
// In src/components/Schedule.tsx
// Update the renderEventContent function:
const renderEventContent = (event)=>{
// Determine if we're on mobile
const isMobile = windowWidth <= 600;
const isExtraSmallMobile = windowWidth <= 480;
// Set font sizes based on screen size (only for event content)
const titleSize = isExtraSmallMobile ? "10px" : isMobile ? "11px" : "14px";
const bodySize = isExtraSmallMobile ? "8px" : isMobile ? "9px" : "12px";
const timeSize = isExtraSmallMobile ? "7px" : isMobile ? "8px" : "10px";
if (event.customContent) {
// For custom HTML content events
if (isMobile) {
// Only modify custom content on mobile
const mobileCustomContent = event.customContent.replace(/font-size:\s*\d+px/g, (match)=>{
// Reduce any explicit font sizes
const currentSize = parseInt(match.replace(/[^0-9]/g, ""));
const newSize = Math.floor(currentSize * 0.8); // Reduce by 30%
return `font-size: ${newSize}px`;
});
return /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("div", {
dangerouslySetInnerHTML: {
__html: mobileCustomContent
}
});
}
return /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("div", {
dangerouslySetInnerHTML: {
__html: event.customContent
}
});
}
if (event.title || event.body) return /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("div", {
className: "event-content",
style: {
padding: "5px",
height: "100%"
},
children: [
event.title && /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("h3", {
style: {
margin: 0,
fontSize: titleSize,
fontWeight: "bold",
whiteSpace: "pre-wrap",
overflow: "hidden",
display: "-webkit-box",
WebkitLineClamp: isMobile ? "1" : "2",
WebkitBoxOrient: "vertical",
lineHeight: "1.2"
},
children: event.title
}),
event.body && /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("p", {
style: {
margin: "2px 0",
fontSize: bodySize,
whiteSpace: "pre-wrap",
overflow: "hidden",
display: "-webkit-box",
WebkitLineClamp: isMobile ? "1" : "3",
WebkitBoxOrient: "vertical",
lineHeight: "1.2"
},
children: event.body
}),
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("div", {
style: {
fontSize: timeSize,
color: "#666",
marginTop: "2px"
},
children: [
event.start,
" - ",
event.end
]
})
]
});
return /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("div", {
className: "event-content",
style: {
padding: "5px"
},
children: /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("div", {
style: {
fontSize: timeSize
},
children: [
event.start,
" - ",
event.end
]
})
});
};
const groupEventsByDay = (events)=>{
const grouped = new Map();
headers.forEach((header)=>{
grouped.set(header.dayIndex, []);
});
events.forEach((event)=>{
if (Array.isArray(event.days)) event.days.forEach((dayIndex)=>{
if (grouped.has(dayIndex)) grouped.get(dayIndex).push(event);
});
});
return grouped;
};
const groupedEvents = groupEventsByDay((0, $5ae523ed11da3f14$export$d425309ca6befbaf)(events));
const timeSlots = Array.from({
length: endHour - startHour
}, (_, i)=>formatHour(i + startHour));
const timeToMinutes = (time)=>{
const [hours, minutes] = time.split(":").map(Number);
return hours * 60 + minutes;
};
const calculateEventPosition = (event)=>{
const totalMinutes = (endHour - startHour) * 60;
const eventStartMinutes = timeToMinutes(event.start) - startHour * 60;
const eventEndMinutes = timeToMinutes(event.end) - startHour * 60;
// Ensure positions are within bounds
const top = Math.max(0, Math.min(100, eventStartMinutes / totalMinutes * 100));
const height = Math.max(0, Math.min(100 - top, (eventEndMinutes - eventStartMinutes) / totalMinutes * 100));
return {
top: top,
height: height
};
};
const renderTimeLines = ()=>Array.from({
length: endHour - startHour
}, (_, i)=>/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("div", {
className: "time-line",
style: {
position: "absolute",
left: 0,
right: 0,
top: `${i * 100 / (endHour - startHour)}%`,
borderTop: "1px solid #e5e5e5",
width: "100%",
pointerEvents: "none",
zIndex: 1
}
}, `timeline-${i}`));
const borderSpacing = "0.625rem"; // 10px equivalent
const timeColumnWidth = "5rem"; // 80px equivalent
const hasEvents = events.length > 0;
return /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("div", {
className: "schedule",
ref: scheduleRef,
style: {
width: "100%",
height: "100%",
position: "absolute",
inset: 0
},
children: [
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("table", {
className: "schedule-table",
style: {
width: "100%",
height: "100%",
borderCollapse: "separate",
borderSpacing: borderSpacing,
backgroundColor: "#f0f0f0",
borderRadius: "20px",
tableLayout: "fixed"
},
children: [
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("colgroup", {
children: [
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("col", {
style: {
width: timeColumnWidth
}
}),
headers.map((_, index)=>/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("col", {}, index))
]
}),
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("thead", {
children: /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("tr", {
style: {
height: "3rem"
},
children: [
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("th", {
className: "time-header",
children: "Time"
}),
headers.map((header, index)=>/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("th", {
children: header.label
}, index))
]
})
}),
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("tbody", {
children: /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("tr", {
style: {
height: "calc(100% - 3rem)"
},
children: [
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("td", {
className: "time-column",
style: {
position: "relative",
backgroundColor: "#ffffff",
borderRadius: "10px",
padding: 0,
verticalAlign: "top",
width: timeColumnWidth
},
children: /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("div", {
style: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
display: "flex",
flexDirection: "column",
height: "100%",
paddingTop: "0.15625rem"
},
children: timeSlots.map((time, index)=>/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("div", {
className: "time-slot",
style: {
flex: `1 0 ${100 / timeSlots.length}%`,
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
fontSize: "0.85em",
color: "#666",
boxSizing: "border-box",
position: "relative",
height: `${100 / timeSlots.length}%`,
borderBottom: index < timeSlots.length - 1 ? "1px solid #e5e5e5" : "none"
},
children: /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("span", {
style: {
position: "absolute",
top: 0,
paddingTop: "0.3125rem"
},
children: time
})
}, index))
})
}),
headers.map((header)=>{
const dayEvents = groupedEvents.get(header.dayIndex) || [];
return /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("td", {
className: "day-column",
style: {
position: "relative",
backgroundColor: "#ffffff",
borderRadius: "10px",
padding: 0,
verticalAlign: "top"
},
children: [
renderTimeLines(),
dayEvents.map((event)=>{
const { top: top, height: height } = calculateEventPosition(event);
return /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("div", {
className: "schedule-event",
style: {
backgroundColor: event.color || "#e0e0e0",
top: `${top}%`,
height: `${height}%`,
minHeight: "1.25rem",
position: "absolute",
left: "0.3125rem",
right: "0.3125rem",
zIndex: 2,
borderRadius: "5px",
cursor: "pointer",
overflow: "hidden",
transition: "transform 0.2s ease-in-out",
fontSize: "0.8em"
},
onClick: ()=>handleEventClick(event),
children: renderEventContent(event)
}, event.id);
})
]
}, header.dayIndex);
})
]
})
})
]
}),
!hasEvents && /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("div", {
style: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(128, 128, 128, 0.1)",
backdropFilter: "blur(2px)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1000,
borderRadius: "20px"
},
children: /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("div", {
style: {
backgroundColor: "white",
padding: "20px 40px",
borderRadius: "10px",
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)",
textAlign: "center"
},
children: /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("p", {
style: {
margin: 0,
fontSize: "1.1em",
color: "#666",
fontWeight: 500
},
children: emptyStateMessage
})
})
}),
selectedEvent && /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)((0, $2c98468b9b21b1f5$export$2e2bcd8739ae039), {
event: selectedEvent,
onClose: ()=>setSelectedEvent(null)
})
]
});
}); // Close the forwardRef callback
var $a93b004b8c50d707$export$2e2bcd8739ae039 = $a93b004b8c50d707$var$Schedule;
// ScheduleHeader component renders the header row of the schedule
const $9d173187df8f14a9$var$ScheduleHeader = ({ headers: headers })=>{
return /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("div", {
className: "schedule-header",
children: headers.map((header, index)=>/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("div", {
className: "header-cell",
children: header
}, index))
});
};
var $9d173187df8f14a9$export$2e2bcd8739ae039 = $9d173187df8f14a9$var$ScheduleHeader;
// ScheduleCell component renders events within a cell of the Schedule
const $32b595b0729c05b4$var$ScheduleCell = ({ events: events, onEventClick: onEventClick })=>{
return /*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("div", {
className: "schedule-cell",
children: events.map((event)=>/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("div", {
className: "schedule-event",
style: {
backgroundColor: event.color
},
onClick: ()=>onEventClick(event),
children: [
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsx)("span", {
className: "event-title",
children: event.title
}),
/*#__PURE__*/ (0, $iEn1Z$reactjsxruntime.jsxs)("span", {
className: "event-time",
children: [
event.start,
" - ",
event.end
]
})
]
}, event.id))
});
};
var $32b595b0729c05b4$export$2e2bcd8739ae039 = $32b595b0729c05b4$var$ScheduleCell;
var $0a52e09d9b2e0488$exports = {};
console.log("Schedule CSS imported");
// Add this line if not already present
$parcel$exportWildcard(module.exports, $0a52e09d9b2e0488$exports);