Schedule Generator

import React, { useState, useCallback } from 'react'; import { Download, Calendar, Clock, Users, Mic, List, FileText, CheckSquare, Camera } from 'lucide-react'; // Helper function to format a date object into 'YYYY-MM-DD' for date inputs const formatDateForInput = (date) => { const d = new Date(date); let month = '' + (d.getMonth() + 1); let day = '' + d.getDate(); const year = d.getFullYear(); if (month.length < 2) month = '0' + month; if (day.length < 2) day = '0' + day; return [year, month, day].join('-'); }; // Helper function to format time in 12-hour format (e.g., 9:00 AM) const formatTime12h = (date) => { return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); }; // Main App Component export default function App() { const today = new Date(); const dayAfterTomorrow = new Date(today); dayAfterTomorrow.setDate(today.getDate() + 2); // State to hold all configuration options from the form const [config, setConfig] = useState({ projectName: 'Q3 User Research', numInterviewers: 3, numInterviews: 24, interviewLength: 75, breakLength: 30, startTime: '09:00', endTime: '17:00', startDate: formatDateForInput(today), endDate: formatDateForInput(dayAfterTomorrow), segmentNames: '18-24 AI Optimist, 25-34 Skeptics, 35-44 Pragmatists', moderatorNames: 'Karen, Ivan, Sarah, Mike', includeObservers: true, }); // State to hold the generated schedule data const [schedule, setSchedule] = useState([]); // Memoized function to generate the schedule based on config const generateSchedule = useCallback(() => { const { numInterviewers, numInterviews, interviewLength, breakLength, startTime, endTime, startDate, endDate, segmentNames, moderatorNames, } = config; // --- Input Validation --- if (numInterviewers <= 0 || numInterviews <= 0 || interviewLength <= 0) { const modal = document.getElementById('error-modal'); if(modal) modal.style.display = 'flex'; return; } const segments = segmentNames.split(',').map(s => s.trim()).filter(Boolean); const moderators = moderatorNames.split(',').map(s => s.trim()).filter(Boolean); if (segments.length === 0 || moderators.length === 0) { const modal = document.getElementById('error-modal'); if(modal) modal.style.display = 'flex'; return; } if (!startDate || !endDate || new Date(startDate) > new Date(endDate)) { const modal = document.getElementById('error-modal'); if(modal) modal.style.display = 'flex'; return; } const newSchedule = []; let participantCount = 0; const start = new Date(startDate); const end = new Date(endDate); start.setMinutes(start.getMinutes() + start.getTimezoneOffset()); end.setMinutes(end.getMinutes() + end.getTimezoneOffset()); for (let d = new Date(start); d <= end && participantCount < numInterviews; d.setDate(d.getDate() + 1)) { const currentDate = new Date(d); newSchedule.push({ isDateRow: true, date: currentDate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }), }); const [startHour, startMinute] = startTime.split(':').map(Number); const [endHour, endMinute] = endTime.split(':').map(Number); let currentTime = new Date(currentDate); currentTime.setHours(startHour, startMinute, 0, 0); const endOfDayTime = new Date(currentDate); endOfDayTime.setHours(endHour, endMinute, 0, 0); while (participantCount < numInterviews) { const slotEndTime = new Date(currentTime.getTime() + interviewLength * 60000); if (slotEndTime > endOfDayTime) { break; } for (let i = 0; i < numInterviewers && participantCount < numInterviews; i++) { participantCount++; const etDateStart = new Date(currentTime); const etDateEnd = new Date(slotEndTime); newSchedule.push({ timeET: `${formatTime12h(etDateStart)}–${formatTime12h(etDateEnd)}`, participantNum: participantCount, participantName: '', segment: segments[(participantCount - 1) % segments.length], homeworkIn: '', zoomRoom: `Room ${i + 1}`, zoomLink: `https://zoom.us/j/123456789${i + 1}`, moderator: moderators[(participantCount - 1) % moderators.length], observer: '', recordingLink: '', notes: '', }); } currentTime = new Date(slotEndTime.getTime() + breakLength * 60000); } } setSchedule(newSchedule); }, [config]); // Function to handle exporting data to CSV format const exportToCSV = () => { if (schedule.length === 0) { const modal = document.getElementById('error-modal'); if(modal) modal.style.display = 'flex'; return; } const headers = [ 'Time (ET)', 'Participant #', 'Participant Name', 'Segment', 'Homework in?', 'Zoom Room', 'Zoom Link', 'Moderator', ...(config.includeObservers ? ['Observer'] : []), 'Recording Link', 'Interview Notes' ]; const csvRows = [headers.join(',')]; schedule.forEach(row => { if (row.isDateRow) { csvRows.push(`"${row.date}"` + ','.repeat(headers.length - 1)); } else { const values = [ row.timeET, row.participantNum, row.participantName, row.segment, row.homeworkIn, row.zoomRoom, row.zoomLink, row.moderator, ...(config.includeObservers ? [row.observer] : []), row.recordingLink, row.notes ]; csvRows.push(values.map(v => `"${v}"`).join(',')); } }); const csvString = csvRows.join('\n'); const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); if (link.href) { URL.revokeObjectURL(link.href); } link.href = URL.createObjectURL(blob); link.download = `${config.projectName.replace(/\s+/g, '_')}_Schedule.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; return (

Interview Schedule Generator

Configure your interview parameters and generate a shareable schedule.

); } // Form Component for User Inputs function ScheduleForm({ config, setConfig, onGenerate }) { const handleInputChange = (e) => { const { name, value, type, checked } = e.target; setConfig(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : (type === 'number' ? (value ? parseFloat(value) : '') : value) })); }; const inputClass = "mt-1 block w-full rounded-md border-slate-300 bg-white shadow-sm focus:border-yellow-500 focus:ring-yellow-500 sm:text-sm p-2 text-slate-900"; const labelClass = "block text-sm font-medium text-slate-700"; return (

Configuration

Optional Columns
); } // Table Component to Display the Schedule function ScheduleTable({ schedule, config, onExport }) { if (schedule.length === 0) { return (

Your schedule will appear here.

Fill out the form and click "Generate Schedule" to begin.

); } const columnCount = [ 'Time (ET)', 'Participant #', 'Participant Name', 'Segment', 'Homework in?', 'Zoom Room', 'Zoom Link', 'Moderator', 'Recording Link', 'Interview Notes' ].length + (config.includeObservers ? 1 : 0); return (

Generated Schedule

All times are in Eastern Time (ET)

{config.includeObservers && } {schedule.map((row, index) => row.isDateRow ? ( ) : ( {config.includeObservers && } ) )}
Time (ET) # Participant Name Segment Homework? Room Zoom Link ModeratorObserverRec. Link Notes
{row.date}
{row.timeET} {row.participantNum}
{row.segment} {row.zoomRoom} Link {row.moderator}
); } function ErrorModal() { const closeModal = () => { const modal = document.getElementById('error-modal'); if(modal) modal.style.display = 'none'; }; return (

Please check your inputs. Ensure all fields are filled correctly, and the end date is not before the start date.

); }

Boom!