Files
my-loves-calendar/components/EventModal.tsx
drjones c373bbdab4 first commit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-16 21:05:27 -08:00

309 lines
14 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Sparkles, Calendar as CalendarIcon, Clock, Briefcase, Heart, Home, Smile, Tag, Trash2 } from 'lucide-react';
import { UserRole, CalendarEvent, EventCategory } from '../types';
import { Button } from './Button';
import { parseNaturalLanguageEvent } from '../services/aiService';
import { getSettings } from '../services/storageService';
import { v4 as uuidv4 } from 'uuid';
interface EventModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (event: CalendarEvent) => void;
onDelete?: (id: string) => void;
currentUserRole: UserRole;
selectedDate?: Date;
initialEvent?: CalendarEvent | null;
}
export const EventModal: React.FC<EventModalProps> = ({
isOpen,
onClose,
onSave,
onDelete,
currentUserRole,
selectedDate,
initialEvent
}) => {
const [activeTab, setActiveTab] = useState<'manual' | 'ai'>('manual');
const [aiPrompt, setAiPrompt] = useState('');
const [isProcessingAi, setIsProcessingAi] = useState(false);
// Form State
const [title, setTitle] = useState('');
const [startDate, setStartDate] = useState('');
const [startTime, setStartTime] = useState('09:00');
const [endTime, setEndTime] = useState('10:00');
const [description, setDescription] = useState('');
const [category, setCategory] = useState<EventCategory>('general');
// Effect to load initial data for editing or reset for new
useEffect(() => {
if (isOpen) {
if (initialEvent) {
setTitle(initialEvent.title);
setDescription(initialEvent.description || '');
setCategory(initialEvent.category);
const start = new Date(initialEvent.startTime);
const end = new Date(initialEvent.endTime);
setStartDate(start.toISOString().split('T')[0]);
setStartTime(start.toTimeString().slice(0, 5));
setEndTime(end.toTimeString().slice(0, 5));
setActiveTab('manual');
} else {
// Reset
setTitle('');
setDescription('');
setCategory('general');
setAiPrompt('');
if (selectedDate) {
const year = selectedDate.getFullYear();
const month = String(selectedDate.getMonth() + 1).padStart(2, '0');
const day = String(selectedDate.getDate()).padStart(2, '0');
setStartDate(`${year}-${month}-${day}`);
} else {
setStartDate(new Date().toISOString().split('T')[0]);
}
setStartTime('09:00');
setEndTime('10:00');
setActiveTab('manual');
}
}
}, [isOpen, initialEvent, selectedDate]);
const handleAiSubmit = async () => {
if (!aiPrompt.trim()) return;
setIsProcessingAi(true);
const settings = getSettings();
const result = await parseNaturalLanguageEvent(aiPrompt, settings);
setIsProcessingAi(false);
if (result) {
setTitle(result.title);
setDescription(result.description || '');
const start = new Date(result.startTime);
const end = new Date(result.endTime);
setStartDate(start.toISOString().split('T')[0]);
setStartTime(start.toTimeString().slice(0, 5));
setEndTime(end.toTimeString().slice(0, 5));
setCategory((result.category as EventCategory) || 'general');
setActiveTab('manual'); // Switch to manual to review
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const startDateTime = new Date(`${startDate}T${startTime}`);
const endDateTime = new Date(`${startDate}T${endTime}`);
const newEvent: CalendarEvent = {
id: initialEvent ? initialEvent.id : uuidv4(),
title,
description,
startTime: startDateTime.toISOString(),
endTime: endDateTime.toISOString(),
createdBy: initialEvent ? initialEvent.createdBy : currentUserRole,
category
};
onSave(newEvent);
onClose();
};
const handleDelete = () => {
if (initialEvent && onDelete) {
onDelete(initialEvent.id);
onClose();
}
};
const categories: { id: EventCategory; label: string; icon: any; color: string }[] = [
{ id: 'general', label: 'General', icon: Tag, color: 'bg-gray-800 text-gray-300 border-gray-600' },
{ id: 'work', label: 'Work', icon: Briefcase, color: 'bg-orange-900/30 text-orange-400 border-orange-800' },
{ id: 'chore', label: 'Chore', icon: Home, color: 'bg-green-900/30 text-green-400 border-green-800' },
{ id: 'date', label: 'Date', icon: Heart, color: 'bg-red-900/30 text-red-400 border-red-800' },
{ id: 'fun', label: 'Fun', icon: Smile, color: 'bg-yellow-900/30 text-yellow-400 border-yellow-800' },
{ id: 'magic', label: 'Magic', icon: Sparkles, color: 'bg-purple-900/30 text-purple-400 border-purple-800' },
];
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40"
onClick={onClose}
/>
<motion.div
initial={{ scale: 0.9, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.9, opacity: 0, y: 20 }}
className="fixed inset-0 flex items-center justify-center z-50 pointer-events-none"
>
<div className="glass-panel w-full max-w-md mx-4 rounded-3xl overflow-hidden pointer-events-auto flex flex-col max-h-[90vh] border border-white/10 shadow-2xl">
{/* Header */}
<div className={`p-6 ${currentUserRole === 'mom' ? 'bg-mom-soft' : 'bg-dad-soft'} flex justify-between items-center`}>
<h2 className={`text-2xl font-extrabold ${currentUserRole === 'mom' ? 'text-mom-primary' : 'text-dad-primary'}`}>
{initialEvent ? 'Edit Event' : 'New Event'}
</h2>
<button onClick={onClose} className="p-2 hover:bg-black/20 rounded-full transition-colors">
<X size={24} className={currentUserRole === 'mom' ? 'text-mom-primary' : 'text-dad-primary'} />
</button>
</div>
{/* Tabs (Only for New Events) */}
{!initialEvent && (
<div className="flex border-b border-white/5 p-2 gap-2 bg-gray-900/30">
<button
onClick={() => setActiveTab('manual')}
className={`flex-1 py-2 rounded-xl text-sm font-bold transition-all ${activeTab === 'manual' ? 'bg-gray-700 text-white' : 'text-gray-500 hover:text-gray-300'}`}
>
Manual Entry
</button>
<button
onClick={() => setActiveTab('ai')}
className={`flex-1 py-2 rounded-xl text-sm font-bold transition-all flex items-center justify-center gap-2 ${activeTab === 'ai' ? 'bg-purple-900/40 text-purple-300 border border-purple-500/20' : 'text-gray-500 hover:text-gray-300'}`}
>
<Sparkles size={16} /> Magic Plan
</button>
</div>
)}
{/* Content */}
<div className="p-6 overflow-y-auto custom-scrollbar">
{activeTab === 'ai' && !initialEvent ? (
<div className="space-y-4">
<div className="bg-purple-900/20 p-4 rounded-2xl border border-purple-500/20">
<p className="text-purple-300 text-sm font-medium mb-2">Try saying...</p>
<p className="text-purple-400/70 text-xs italic">"Dinner with Mom next Friday at 7pm"</p>
<p className="text-purple-400/70 text-xs italic">"Weekend getaway to the beach July 12-14"</p>
</div>
<textarea
value={aiPrompt}
onChange={(e) => setAiPrompt(e.target.value)}
placeholder="Describe your event..."
className="w-full h-32 p-4 rounded-2xl bg-gray-800 border-2 border-transparent focus:border-purple-500 focus:ring-0 resize-none transition-colors text-white placeholder-gray-500"
/>
<Button
onClick={handleAiSubmit}
disabled={!aiPrompt.trim()}
isLoading={isProcessingAi}
className="w-full bg-purple-600 hover:bg-purple-500 text-white"
>
<Sparkles size={18} /> Generate Details
</Button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-1">
<label className="text-xs font-bold text-gray-500 uppercase tracking-wider ml-1">Event Title</label>
<input
type="text"
required
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="What's happening?"
className="w-full p-4 rounded-2xl bg-gray-800 border-transparent focus:bg-gray-700 focus:ring-2 focus:ring-gray-600 focus:border-transparent transition-all font-bold text-lg text-white placeholder-gray-500"
/>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-gray-500 uppercase tracking-wider ml-1">Smart Category</label>
<div className="flex gap-2 overflow-x-auto pb-2 scrollbar-none">
{categories.map((cat) => {
const Icon = cat.icon;
const isSelected = category === cat.id;
return (
<button
key={cat.id}
type="button"
onClick={() => setCategory(cat.id)}
className={`
flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs font-bold transition-all whitespace-nowrap border-2
${isSelected ? `${cat.color} border-current` : 'bg-gray-800 text-gray-500 border-transparent hover:bg-gray-700'}
`}
>
<Icon size={14} />
{cat.label}
</button>
);
})}
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-xs font-bold text-gray-500 uppercase tracking-wider ml-1">Date</label>
<div className="relative">
<input
type="date"
required
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="w-full p-3 pl-10 rounded-2xl bg-gray-800 border-transparent focus:bg-gray-700 focus:ring-2 focus:ring-gray-600 text-white"
/>
<CalendarIcon className="absolute left-3 top-3.5 text-gray-500" size={18} />
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-gray-500 uppercase tracking-wider ml-1">Time</label>
<div className="flex gap-2 items-center">
<input
type="time"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
className="w-full p-3 rounded-2xl bg-gray-800 border-transparent focus:bg-gray-700 focus:ring-2 focus:ring-gray-600 text-sm text-white"
/>
<span className="text-gray-500">-</span>
<input
type="time"
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
className="w-full p-3 rounded-2xl bg-gray-800 border-transparent focus:bg-gray-700 focus:ring-2 focus:ring-gray-600 text-sm text-white"
/>
</div>
</div>
</div>
<div className="space-y-1">
<label className="text-xs font-bold text-gray-500 uppercase tracking-wider ml-1">Notes</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Add details, location, etc."
className="w-full p-4 rounded-2xl bg-gray-800 border-transparent focus:bg-gray-700 focus:ring-2 focus:ring-gray-600 focus:border-transparent transition-all resize-none h-24 text-white placeholder-gray-500"
/>
</div>
<div className="pt-2 flex gap-3">
{initialEvent && (
<Button type="button" variant="danger" onClick={handleDelete} className="flex-1">
<Trash2 size={18} /> Delete
</Button>
)}
<Button type="submit" roleTheme={currentUserRole} className="flex-[2]">
{initialEvent ? 'Save Changes' : 'Create Event'}
</Button>
</div>
</form>
)}
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
};