308
components/EventModal.tsx
Normal file
308
components/EventModal.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user