follow-up / index.html
localhost-llm's picture
Add 2 files
c139d2c verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Follow-Up Calendar</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.calendar-day:hover {
transform: scale(1.05);
transition: transform 0.2s ease;
}
.event-dot {
width: 8px;
height: 8px;
border-radius: 50%;
}
.modal-overlay {
background-color: rgba(0, 0, 0, 0.5);
}
.fade-in {
animation: fadeIn 0.3s ease-in-out;
}
.event-card {
transition: all 0.2s ease;
}
.event-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.section-toggle {
transition: all 0.3s ease;
}
.section-toggle:hover {
background-color: rgba(59, 130, 246, 0.1);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@media (max-width: 640px) {
.month-grid {
grid-template-columns: repeat(7, minmax(0, 1fr));
}
.week-grid {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.week-day-header {
display: none;
}
.week-day-mobile {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
border-bottom: 1px solid #e5e7eb;
}
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
const FollowUpCalendar = () => {
const [currentDate, setCurrentDate] = useState(new Date());
const [events, setEvents] = useState([]);
const [showModal, setShowModal] = useState(false);
const [selectedDate, setSelectedDate] = useState(null);
const [newEvent, setNewEvent] = useState({
title: '',
description: '',
time: '09:00',
type: 'meeting'
});
const [viewMode, setViewMode] = useState('month'); // 'month' or 'week'
const [selectedEvent, setSelectedEvent] = useState(null);
const [showCompleted, setShowCompleted] = useState(false);
const [showAll, setShowAll] = useState(false);
// Sample initial events
useEffect(() => {
const sampleEvents = [
{
id: 1,
title: 'Client Meeting',
description: 'Discuss project requirements with ABC Corp',
date: new Date(new Date().setDate(new Date().getDate() + 1)),
time: '10:30',
type: 'meeting',
completed: false
},
{
id: 2,
title: 'Follow-up Call',
description: 'Check on product delivery status',
date: new Date(new Date().setDate(new Date().getDate() + 3)),
time: '14:00',
type: 'call',
completed: false
},
{
id: 3,
title: 'Email Reminder',
description: 'Send proposal to XYZ Inc',
date: new Date(new Date().setDate(new Date().getDate() - 2)),
time: '11:15',
type: 'email',
completed: true
},
{
id: 4,
title: 'Project Review',
description: 'Review project milestones with team',
date: new Date(new Date().setDate(new Date().getDate() - 5)),
time: '15:30',
type: 'meeting',
completed: true
}
];
setEvents(sampleEvents);
}, []);
const navigateMonth = (direction) => {
const newDate = new Date(currentDate);
newDate.setMonth(newDate.getMonth() + direction);
setCurrentDate(newDate);
};
const navigateWeek = (direction) => {
const newDate = new Date(currentDate);
newDate.setDate(newDate.getDate() + (direction * 7));
setCurrentDate(newDate);
};
const getDaysInMonth = () => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDay = firstDay.getDay();
const days = [];
// Add empty cells for days before the first day of the month
for (let i = 0; i < startingDay; i++) {
days.push(null);
}
// Add days of the month
for (let i = 1; i <= daysInMonth; i++) {
const date = new Date(year, month, i);
days.push(date);
}
return days;
};
const getWeekDays = () => {
const weekStart = new Date(currentDate);
weekStart.setDate(weekStart.getDate() - weekStart.getDay());
const weekDays = [];
for (let i = 0; i < 7; i++) {
const day = new Date(weekStart);
day.setDate(day.getDate() + i);
weekDays.push(day);
}
return weekDays;
};
const getEventsForDate = (date) => {
if (!date) return [];
return events.filter(event =>
event.date.getDate() === date.getDate() &&
event.date.getMonth() === date.getMonth() &&
event.date.getFullYear() === date.getFullYear()
);
};
const handleDateClick = (date) => {
if (!date) return;
setSelectedDate(date);
setShowModal(true);
setNewEvent({
title: '',
description: '',
time: '09:00',
type: 'meeting'
});
};
const handleEventClick = (event, e) => {
e.stopPropagation();
setSelectedEvent(event);
};
const handleAddEvent = () => {
if (!newEvent.title || !selectedDate) return;
const event = {
id: events.length + 1,
title: newEvent.title,
description: newEvent.description,
date: new Date(selectedDate),
time: newEvent.time,
type: newEvent.type,
completed: false
};
setEvents([...events, event]);
setShowModal(false);
};
const handleDeleteEvent = (id) => {
setEvents(events.filter(event => event.id !== id));
setSelectedEvent(null);
};
const toggleEventCompletion = (id) => {
setEvents(events.map(event =>
event.id === id ? { ...event, completed: !event.completed } : event
));
};
const isToday = (date) => {
if (!date) return false;
const today = new Date();
return date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
};
const getEventTypeColor = (type) => {
switch (type) {
case 'meeting': return 'bg-blue-500';
case 'call': return 'bg-green-500';
case 'email': return 'bg-purple-500';
case 'task': return 'bg-yellow-500';
default: return 'bg-gray-500';
}
};
const getEventTypeIcon = (type) => {
switch (type) {
case 'meeting': return 'fa-users';
case 'call': return 'fa-phone';
case 'email': return 'fa-envelope';
case 'task': return 'fa-check-circle';
default: return 'fa-calendar';
}
};
const renderMonthView = () => {
const days = getDaysInMonth();
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return (
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="grid grid-cols-7 gap-1 p-2 bg-gray-100">
{weekdays.map(day => (
<div key={day} className="text-center font-medium text-gray-600 py-2 text-xs sm:text-sm">
{day}
</div>
))}
</div>
<div className="grid month-grid grid-cols-7 gap-1 p-2">
{days.map((date, index) => {
const dateEvents = getEventsForDate(date);
return (
<div
key={index}
onClick={() => handleDateClick(date)}
className={`calendar-day min-h-16 sm:min-h-24 p-1 sm:p-2 border rounded-lg cursor-pointer transition-all ${date ? 'hover:bg-gray-50' : 'bg-gray-50'} ${isToday(date) ? 'border-blue-400 border-2' : 'border-gray-200'}`}
>
{date && (
<>
<div className="flex justify-between items-center mb-1">
<span className={`font-medium text-xs sm:text-sm ${isToday(date) ? 'text-blue-600' : 'text-gray-700'}`}>
{date.getDate()}
</span>
{dateEvents.length > 0 && (
<div className="flex space-x-1">
{dateEvents.slice(0, 2).map(event => (
<div
key={event.id}
className={`event-dot ${getEventTypeColor(event.type)}`}
/>
))}
{dateEvents.length > 2 && <span className="text-xs text-gray-500 hidden sm:inline">+{dateEvents.length - 2}</span>}
</div>
)}
</div>
<div className="space-y-1 overflow-hidden hidden sm:block">
{dateEvents.slice(0, 2).map(event => (
<div
key={event.id}
onClick={(e) => handleEventClick(event, e)}
className={`text-xs p-1 rounded truncate ${getEventTypeColor(event.type)} text-white`}
>
{event.time} {event.title}
</div>
))}
</div>
</>
)}
</div>
);
})}
</div>
</div>
);
};
const renderWeekView = () => {
const weekDays = getWeekDays();
return (
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="grid week-day-header grid-cols-7 gap-1 p-2 bg-gray-100">
{weekDays.map(day => {
const dateEvents = getEventsForDate(day);
return (
<div key={day} className="text-center">
<div className="font-medium text-gray-600 text-xs sm:text-sm">
{day.toLocaleDateString('en-US', { weekday: 'short' })}
</div>
<div
className={`mx-auto w-6 h-6 sm:w-8 sm:h-8 flex items-center justify-center rounded-full text-xs sm:text-sm ${isToday(day) ? 'bg-blue-500 text-white' : 'text-gray-700'}`}
>
{day.getDate()}
</div>
</div>
);
})}
</div>
<div className="grid week-grid grid-cols-1 sm:grid-cols-7 gap-2 sm:gap-4 p-2 sm:p-4 min-h-96">
{weekDays.map(day => {
const dateEvents = getEventsForDate(day);
return (
<div key={day} className="border rounded-lg">
<div
onClick={() => handleDateClick(day)}
className={`week-day-mobile sm:hidden ${isToday(day) ? 'bg-blue-50' : ''}`}
>
<span className="font-medium">
{day.toLocaleDateString('en-US', { weekday: 'short' })}, {day.getDate()}
</span>
{dateEvents.length > 0 && (
<span className="text-xs bg-gray-200 px-2 py-1 rounded-full">
{dateEvents.length} event{dateEvents.length !== 1 ? 's' : ''}
</span>
)}
</div>
<div
onClick={() => handleDateClick(day)}
className={`p-2 cursor-pointer ${isToday(day) ? 'border-blue-400' : 'border-gray-200'}`}
>
<div className="space-y-2">
{dateEvents.length > 0 ? (
dateEvents.map(event => (
<div
key={event.id}
onClick={(e) => handleEventClick(event, e)}
className={`p-2 rounded-lg ${getEventTypeColor(event.type)} text-white`}
>
<div className="flex items-center">
<i className={`fas ${getEventTypeIcon(event.type)} mr-2 text-xs sm:text-sm`}></i>
<div className="truncate">
<div className="font-medium text-xs sm:text-sm">{event.title}</div>
<div className="text-xs">{event.time}</div>
</div>
</div>
</div>
))
) : (
<div className="text-gray-400 text-center py-4 text-xs sm:text-sm">No events</div>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
);
};
const renderEventCard = (event) => {
return (
<div
key={event.id}
onClick={() => setSelectedEvent(event)}
className={`event-card p-3 sm:p-4 rounded-lg cursor-pointer ${getEventTypeColor(event.type)} text-white mb-2 sm:mb-3`}
>
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="font-medium text-sm sm:text-base mb-1">{event.title}</div>
<div className="text-xs sm:text-sm mb-1">{event.date.toLocaleDateString()} at {event.time}</div>
{event.description && (
<div className="text-xs opacity-90 truncate">{event.description}</div>
)}
</div>
<div className="flex items-center space-x-2">
<i className={`fas ${getEventTypeIcon(event.type)} text-sm sm:text-base`}></i>
<button
onClick={(e) => {
e.stopPropagation();
toggleEventCompletion(event.id);
}}
className={`w-5 h-5 rounded-full flex items-center justify-center ${event.completed ? 'bg-white text-green-500' : 'bg-white bg-opacity-30'}`}
>
{event.completed && <i className="fas fa-check text-xs"></i>}
</button>
</div>
</div>
</div>
);
};
return (
<div className="min-h-screen bg-gray-100 p-2 sm:p-4 md:p-8">
<div className="max-w-6xl mx-auto">
<div className="flex flex-col sm:flex-row justify-between items-center mb-4 sm:mb-6">
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-800 mb-2 sm:mb-0">
Follow-Up Calendar
</h1>
<div className="flex space-x-2 sm:space-x-4">
<button
onClick={() => setViewMode('month')}
className={`px-3 py-1 sm:px-4 sm:py-2 rounded-lg text-xs sm:text-sm ${viewMode === 'month' ? 'bg-blue-500 text-white' : 'bg-white text-gray-700'}`}
>
Month
</button>
<button
onClick={() => setViewMode('week')}
className={`px-3 py-1 sm:px-4 sm:py-2 rounded-lg text-xs sm:text-sm ${viewMode === 'week' ? 'bg-blue-500 text-white' : 'bg-white text-gray-700'}`}
>
Week
</button>
</div>
</div>
<div className="bg-white rounded-lg shadow p-2 sm:p-4 mb-4 sm:mb-6">
<div className="flex flex-col sm:flex-row justify-between items-center mb-3 sm:mb-4">
<div className="flex items-center space-x-2 sm:space-x-4 mb-2 sm:mb-0">
<button
onClick={viewMode === 'month' ? () => navigateMonth(-1) : () => navigateWeek(-1)}
className="p-1 sm:p-2 rounded-full hover:bg-gray-100"
>
<i className="fas fa-chevron-left text-gray-600 text-sm sm:text-base"></i>
</button>
<h2 className="text-lg sm:text-xl font-semibold text-gray-800">
{currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
</h2>
<button
onClick={viewMode === 'month' ? () => navigateMonth(1) : () => navigateWeek(1)}
className="p-1 sm:p-2 rounded-full hover:bg-gray-100"
>
<i className="fas fa-chevron-right text-gray-600 text-sm sm:text-base"></i>
</button>
<button
onClick={() => setCurrentDate(new Date())}
className="px-2 py-1 sm:px-3 sm:py-1 text-xs sm:text-sm bg-gray-200 rounded-lg hover:bg-gray-300"
>
Today
</button>
</div>
<button
onClick={() => {
setSelectedDate(new Date());
setShowModal(true);
}}
className="px-3 py-1 sm:px-4 sm:py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 flex items-center text-xs sm:text-sm"
>
<i className="fas fa-plus mr-1 sm:mr-2 text-xs sm:text-sm"></i>
Add Event
</button>
</div>
{viewMode === 'month' ? renderMonthView() : renderWeekView()}
</div>
{/* Upcoming Follow-Ups */}
<div className="bg-white rounded-lg shadow p-4 sm:p-6 mb-4 sm:mb-6">
<h3 className="text-base sm:text-lg font-semibold text-gray-800 mb-3 sm:mb-4">Upcoming Follow-Ups</h3>
<div className="space-y-3 sm:space-y-4">
{events
.filter(event => !event.completed && event.date >= new Date())
.sort((a, b) => a.date - b.date)
.slice(0, 5)
.map(event => renderEventCard(event))}
{events.filter(event => !event.completed && event.date >= new Date()).length === 0 && (
<div className="text-center py-4 text-gray-500 text-sm">No upcoming follow-ups</div>
)}
</div>
</div>
{/* Completed Follow-Ups */}
<div className="bg-white rounded-lg shadow p-4 sm:p-6 mb-4 sm:mb-6">
<div
className="flex justify-between items-center cursor-pointer mb-3 sm:mb-4 section-toggle p-2 rounded-lg"
onClick={() => setShowCompleted(!showCompleted)}
>
<h3 className="text-base sm:text-lg font-semibold text-gray-800">Completed Follow-Ups</h3>
<i className={`fas ${showCompleted ? 'fa-chevron-up' : 'fa-chevron-down'} text-gray-500`}></i>
</div>
{showCompleted && (
<div className="space-y-3 sm:space-y-4">
{events
.filter(event => event.completed)
.sort((a, b) => b.date - a.date)
.slice(0, 5)
.map(event => renderEventCard(event))}
{events.filter(event => event.completed).length === 0 && (
<div className="text-center py-4 text-gray-500 text-sm">No completed follow-ups</div>
)}
</div>
)}
</div>
{/* All Follow-Ups */}
<div className="bg-white rounded-lg shadow p-4 sm:p-6">
<div
className="flex justify-between items-center cursor-pointer mb-3 sm:mb-4 section-toggle p-2 rounded-lg"
onClick={() => setShowAll(!showAll)}
>
<h3 className="text-base sm:text-lg font-semibold text-gray-800">All Follow-Ups</h3>
<i className={`fas ${showAll ? 'fa-chevron-up' : 'fa-chevron-down'} text-gray-500`}></i>
</div>
{showAll && (
<div className="space-y-3 sm:space-y-4">
{events
.sort((a, b) => a.date - b.date)
.map(event => renderEventCard(event))}
{events.length === 0 && (
<div className="text-center py-4 text-gray-500 text-sm">No follow-ups created yet</div>
)}
</div>
)}
</div>
</div>
{/* Add Event Modal */}
{showModal && (
<div className="fixed inset-0 flex items-center justify-center z-50 modal-overlay fade-in p-2 sm:p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-2 sm:mx-4">
<div className="p-4 sm:p-6">
<div className="flex justify-between items-center mb-3 sm:mb-4">
<h3 className="text-lg sm:text-xl font-semibold text-gray-800">
{selectedDate?.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}
</h3>
<button
onClick={() => setShowModal(false)}
className="text-gray-500 hover:text-gray-700"
>
<i className="fas fa-times"></i>
</button>
</div>
<div className="space-y-3 sm:space-y-4">
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1">Event Type</label>
<div className="grid grid-cols-4 gap-1 sm:gap-2">
{['meeting', 'call', 'email', 'task'].map(type => (
<button
key={type}
onClick={() => setNewEvent({...newEvent, type})}
className={`p-1 sm:p-2 rounded-lg flex flex-col items-center text-xs ${newEvent.type === type ? getEventTypeColor(type) + ' text-white' : 'bg-gray-100 text-gray-700'}`}
>
<i className={`fas ${getEventTypeIcon(type)} mb-1 text-xs sm:text-sm`}></i>
<span className="capitalize">{type}</span>
</button>
))}
</div>
</div>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1">Title</label>
<input
type="text"
value={newEvent.title}
onChange={(e) => setNewEvent({...newEvent, title: e.target.value})}
className="w-full px-2 sm:px-3 py-1 sm:py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 text-xs sm:text-sm"
placeholder="Enter event title"
/>
</div>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1">Time</label>
<input
type="time"
value={newEvent.time}
onChange={(e) => setNewEvent({...newEvent, time: e.target.value})}
className="w-full px-2 sm:px-3 py-1 sm:py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 text-xs sm:text-sm"
/>
</div>
<div>
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1">Description</label>
<textarea
value={newEvent.description}
onChange={(e) => setNewEvent({...newEvent, description: e.target.value})}
className="w-full px-2 sm:px-3 py-1 sm:py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500 text-xs sm:text-sm"
rows="3"
placeholder="Enter event details"
></textarea>
</div>
<div className="flex justify-end space-x-2 sm:space-x-3 pt-3 sm:pt-4">
<button
onClick={() => setShowModal(false)}
className="px-3 sm:px-4 py-1 sm:py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 text-xs sm:text-sm"
>
Cancel
</button>
<button
onClick={handleAddEvent}
className="px-3 sm:px-4 py-1 sm:py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 text-xs sm:text-sm"
>
Add Event
</button>
</div>
</div>
</div>
</div>
</div>
)}
{/* Event Detail Modal */}
{selectedEvent && (
<div className="fixed inset-0 flex items-center justify-center z-50 modal-overlay fade-in p-2 sm:p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-2 sm:mx-4">
<div className="p-4 sm:p-6">
<div className="flex justify-between items-center mb-3 sm:mb-4">
<h3 className="text-lg sm:text-xl font-semibold text-gray-800">
{selectedEvent.title}
</h3>
<button
onClick={() => setSelectedEvent(null)}
className="text-gray-500 hover:text-gray-700"
>
<i className="fas fa-times"></i>
</button>
</div>
<div className={`p-3 sm:p-4 rounded-lg mb-3 sm:mb-4 ${getEventTypeColor(selectedEvent.type)} text-white`}>
<div className="flex items-center mb-1 sm:mb-2">
<i className={`fas ${getEventTypeIcon(selectedEvent.type)} mr-2 text-sm sm:text-xl`}></i>
<span className="capitalize font-medium text-xs sm:text-sm">{selectedEvent.type}</span>
</div>
<div className="text-xs sm:text-sm">
{selectedEvent.date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })} at {selectedEvent.time}
</div>
</div>
<div className="mb-3 sm:mb-4">
<h4 className="text-xs sm:text-sm font-medium text-gray-700 mb-1">Description</h4>
<p className="text-gray-800 text-xs sm:text-sm">{selectedEvent.description || 'No description provided'}</p>
</div>
<div className="flex items-center mb-4 sm:mb-5">
<span className="text-xs sm:text-sm font-medium text-gray-700 mr-2">Status:</span>
<button
onClick={() => toggleEventCompletion(selectedEvent.id)}
className={`px-3 py-1 rounded-lg text-xs sm:text-sm ${selectedEvent.completed ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`}
>
{selectedEvent.completed ? 'Completed' : 'Pending'}
</button>
</div>
<div className="flex justify-end space-x-2 sm:space-x-3 pt-3 sm:pt-4">
<button
onClick={() => handleDeleteEvent(selectedEvent.id)}
className="px-3 sm:px-4 py-1 sm:py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 text-xs sm:text-sm"
>
Delete
</button>
<button
onClick={() => {
setSelectedDate(selectedEvent.date);
setNewEvent({
title: selectedEvent.title,
description: selectedEvent.description,
time: selectedEvent.time,
type: selectedEvent.type
});
setSelectedEvent(null);
setShowModal(true);
}}
className="px-3 sm:px-4 py-1 sm:py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 text-xs sm:text-sm"
>
Edit
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<FollowUpCalendar />);
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=localhost-llm/follow-up" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>