first commit

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
drjones
2026-02-16 21:05:27 -08:00
commit c373bbdab4
25 changed files with 2964 additions and 0 deletions

View File

@@ -0,0 +1,187 @@
import React, { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Send, Bot, User as UserIcon, X, Loader2 } from 'lucide-react';
import { ChatMessage, UserRole, AppSettings, CalendarEvent } from '../types';
import { generateChatResponse } from '../services/aiService';
interface AssistantViewProps {
isOpen: boolean;
onClose: () => void;
currentUser: UserRole;
settings: AppSettings;
onEventCreated: (event: CalendarEvent) => void;
}
export const AssistantView: React.FC<AssistantViewProps> = ({
isOpen,
onClose,
currentUser,
settings,
onEventCreated
}) => {
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: 'welcome',
role: 'assistant',
content: `Hi ${currentUser === 'mom' ? 'Mom' : 'Dad'}! I am your cosmic assistant. How can I help you plan?`,
timestamp: new Date()
}
]);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages, isOpen]);
const handleSend = async (e?: React.FormEvent) => {
e?.preventDefault();
if (!input.trim() || isLoading) return;
const userMsg: ChatMessage = {
id: Date.now().toString(),
role: 'user',
content: input,
timestamp: new Date()
};
setMessages(prev => [...prev, userMsg]);
setInput('');
setIsLoading(true);
const { text, event } = await generateChatResponse(input, settings, currentUser);
const assistantMsg: ChatMessage = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: text,
timestamp: new Date()
};
setMessages(prev => [...prev, assistantMsg]);
setIsLoading(false);
if (event) {
onEventCreated(event);
}
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40"
/>
{/* Side Panel */}
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
className="fixed inset-y-0 right-0 w-full md:w-[450px] bg-gray-900 border-l border-gray-800 shadow-2xl z-50 flex flex-col"
>
{/* Header */}
<div className="p-4 border-b border-gray-800 flex justify-between items-center bg-gray-900/95 backdrop-blur">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-purple-600 to-blue-600 flex items-center justify-center shadow-lg shadow-purple-900/50">
<Bot className="text-white" size={24} />
</div>
<div>
<h2 className="text-lg font-bold text-white">Home Assistant</h2>
<div className="flex items-center gap-1.5">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></div>
<span className="text-xs text-gray-400 font-mono">
{settings.aiProvider === 'ollama' ? 'Local Spirit' : 'Gemini Cloud'}
</span>
</div>
</div>
</div>
<button onClick={onClose} className="p-2 hover:bg-gray-800 rounded-full text-gray-400 hover:text-white transition-colors">
<X size={24} />
</button>
</div>
{/* Chat Area */}
<div className="flex-1 overflow-y-auto p-4 space-y-4 custom-scrollbar">
{messages.map((msg) => (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
key={msg.id}
className={`flex gap-3 ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'}`}
>
<div className={`
w-8 h-8 rounded-full flex items-center justify-center shrink-0
${msg.role === 'user'
? (currentUser === 'mom' ? 'bg-mom-primary' : 'bg-dad-primary')
: 'bg-gray-700'}
`}>
{msg.role === 'user' ? <UserIcon size={14} className="text-white" /> : <Bot size={14} className="text-purple-300" />}
</div>
<div className={`
max-w-[80%] p-3 rounded-2xl text-sm leading-relaxed
${msg.role === 'user'
? 'bg-gray-800 text-white rounded-tr-none border border-gray-700'
: 'bg-purple-900/20 text-gray-200 rounded-tl-none border border-purple-500/20'}
`}>
{msg.content}
<div className="text-[10px] opacity-30 mt-1 text-right">
{msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</motion.div>
))}
{isLoading && (
<div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-gray-700 flex items-center justify-center shrink-0">
<Bot size={14} className="text-purple-300" />
</div>
<div className="bg-purple-900/20 p-3 rounded-2xl rounded-tl-none border border-purple-500/20 flex items-center gap-2">
<Loader2 size={16} className="animate-spin text-purple-400" />
<span className="text-xs text-purple-300">Consulting the stars...</span>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Input Area */}
<div className="p-4 bg-gray-900/95 backdrop-blur border-t border-gray-800">
<form onSubmit={handleSend} className="relative">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Schedule a date on Friday at 7pm..."
className="w-full bg-gray-800 text-white pl-4 pr-12 py-3 rounded-xl border border-gray-700 focus:border-purple-500 focus:ring-1 focus:ring-purple-500 outline-none placeholder-gray-500 transition-all"
disabled={isLoading}
/>
<button
type="submit"
disabled={!input.trim() || isLoading}
className="absolute right-2 top-2 p-1.5 bg-purple-600 hover:bg-purple-500 text-white rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
<Send size={18} />
</button>
</form>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
};

52
components/Button.tsx Normal file
View File

@@ -0,0 +1,52 @@
import React from 'react';
import { motion } from 'framer-motion';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
roleTheme?: 'mom' | 'dad' | 'neutral';
isLoading?: boolean;
}
export const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
roleTheme = 'neutral',
isLoading,
className = '',
...props
}) => {
let baseStyles = "px-6 py-3 rounded-2xl font-bold transition-all duration-200 flex items-center justify-center gap-2 outline-none focus:ring-4 disabled:opacity-50 disabled:cursor-not-allowed";
const getThemeColors = () => {
if (variant === 'ghost') return "bg-transparent hover:bg-gray-100 text-gray-600 focus:ring-gray-200";
if (variant === 'danger') return "bg-red-100 text-red-600 hover:bg-red-200 focus:ring-red-100";
if (variant === 'secondary') return "bg-white text-gray-700 border-2 border-gray-100 hover:border-gray-200 focus:ring-gray-100";
switch (roleTheme) {
case 'mom':
return "bg-mom-primary text-white hover:bg-pink-500 shadow-lg shadow-pink-200 focus:ring-pink-200";
case 'dad':
return "bg-dad-primary text-white hover:bg-blue-500 shadow-lg shadow-blue-200 focus:ring-blue-200";
default:
return "bg-gray-800 text-white hover:bg-gray-900 shadow-lg shadow-gray-300 focus:ring-gray-200";
}
};
return (
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.95 }}
className={`${baseStyles} ${getThemeColors()} ${className}`}
disabled={isLoading || props.disabled}
{...props}
>
{isLoading ? (
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
) : children}
</motion.button>
);
};

458
components/CalendarView.tsx Normal file
View File

@@ -0,0 +1,458 @@
import React, { useState, useMemo, useEffect } from 'react';
import {
format, startOfMonth, endOfMonth, startOfWeek, endOfWeek,
eachDayOfInterval, isSameMonth, isSameDay, addMonths, isToday, parseISO
} from 'date-fns';
import { motion, AnimatePresence, PanInfo } from 'framer-motion';
import { ChevronLeft, ChevronRight, Plus, LogOut, Settings as SettingsIcon, Sparkles, Bot, Moon, Sun, Zap, Cloud, Wand2, Trash2, Calendar as CalIcon, Activity, X, ChevronDown } from 'lucide-react';
import { CalendarEvent, UserRole, UserStatus, AppSettings } from '../types';
import { Button } from './Button';
import { analyzeScheduleAndSuggest } from '../services/aiService';
import { Candle } from './Candle';
import { CosmicPulseModal } from './CosmicPulseModal';
interface CalendarViewProps {
events: CalendarEvent[];
currentUser: UserRole;
currentStatus: UserStatus;
onStatusChange: (status: UserStatus) => void;
onAddEvent: (date?: Date) => void;
onEditEvent: (event: CalendarEvent) => void;
onMoveEvent: (eventId: string, newDate: Date) => void;
onLogout: () => void;
onDeleteEvent: (id: string) => void;
onOpenSettings: () => void;
onOpenAssistant: () => void;
settings: AppSettings;
}
export const CalendarView: React.FC<CalendarViewProps> = ({
events, currentUser, currentStatus, onStatusChange, onAddEvent, onEditEvent,
onLogout, onDeleteEvent, onMoveEvent, onOpenSettings, onOpenAssistant, settings
}) => {
const [currentDate, setCurrentDate] = useState(new Date());
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const [direction, setDirection] = useState(0);
const [showOracle, setShowOracle] = useState(false);
const [oracleWisdom, setOracleWisdom] = useState<string[]>([]);
const [isConsulting, setIsConsulting] = useState(false);
const [isEditMode, setIsEditMode] = useState(false);
const [dragOverDate, setDragOverDate] = useState<string | null>(null);
const [isPulseOpen, setIsPulseOpen] = useState(false);
const [isAgendaOpen, setIsAgendaOpen] = useState(false); // New state to control sheet visibility
// Mock partner
const partnerRole: UserRole = currentUser === 'mom' ? 'dad' : 'mom';
const [partnerStatus, setPartnerStatus] = useState<UserStatus>('offline');
useEffect(() => {
const partnerTimer = setInterval(() => {
const statuses: UserStatus[] = ['online', 'busy', 'casting_spells', 'thinking'];
setPartnerStatus(statuses[Math.floor(Math.random() * statuses.length)]);
}, 10000);
return () => clearInterval(partnerTimer);
}, []);
const days = useMemo(() => {
const monthStart = startOfMonth(currentDate);
const startDate = startOfWeek(monthStart);
const endDate = endOfWeek(endOfMonth(monthStart));
return eachDayOfInterval({ start: startDate, end: endDate });
}, [currentDate]);
const changeMonth = (val: number) => {
setDirection(val);
setCurrentDate(prev => addMonths(prev, val));
};
const getDayEvents = (day: Date) => {
const dayEvents = events.filter(event => isSameDay(parseISO(event.startTime), day));
return dayEvents.sort((a, b) => {
if (a.category === 'magic' && b.category !== 'magic') return -1;
if (a.category !== 'magic' && b.category === 'magic') return 1;
return new Date(a.startTime).getTime() - new Date(b.startTime).getTime();
});
};
const handleConsultOracle = async () => {
if (showOracle) { setShowOracle(false); return; }
setIsConsulting(true);
setShowOracle(true);
const wisdom = await analyzeScheduleAndSuggest(events, settings);
setOracleWisdom(wisdom);
setIsConsulting(false);
};
const getStatusIcon = (status: UserStatus) => {
switch (status) {
case 'online': return <Sun size={14} className="text-yellow-400" />;
case 'busy': return <Zap size={14} className="text-red-400" />;
case 'sleeping': return <Moon size={14} className="text-blue-300" />;
case 'casting_spells': return <Sparkles size={14} className="text-purple-400" />;
case 'thinking': return <Cloud size={14} className="text-gray-400" />;
default: return <div className="w-2 h-2 rounded-full bg-gray-500" />;
}
};
const statusOptions: {id: UserStatus, label: string, icon: any}[] = [
{ id: 'online', label: 'Present', icon: Sun },
{ id: 'busy', label: 'Busy', icon: Zap },
{ id: 'casting_spells', label: 'Magic', icon: Sparkles },
{ id: 'thinking', label: 'Thinking', icon: Cloud },
{ id: 'sleeping', label: 'Resting', icon: Moon },
];
const handleDragStart = (e: React.DragEvent, eventId: string) => {
if (!isEditMode) return;
e.dataTransfer.setData('eventId', eventId);
};
const handleDragOver = (e: React.DragEvent, dateStr: string) => {
if (!isEditMode) return;
e.preventDefault();
setDragOverDate(dateStr);
};
const handleDrop = (e: React.DragEvent, date: Date) => {
if (!isEditMode) return;
e.preventDefault();
const eventId = e.dataTransfer.getData('eventId');
if (eventId) onMoveEvent(eventId, date);
setDragOverDate(null);
};
const handleTrashDrop = (e: React.DragEvent) => {
if (!isEditMode) return;
e.preventDefault();
const eventId = e.dataTransfer.getData('eventId');
if (eventId) onDeleteEvent(eventId);
};
const handleDayClick = (day: Date) => {
setSelectedDate(day);
setIsAgendaOpen(true); // Open sheet on tap
if (isSameDay(day, selectedDate) && !isEditMode) {
// Optional double tap behavior logic
}
};
const handleSheetDragEnd = (event: MouseEvent | TouchEvent | PointerEvent, info: PanInfo) => {
if (info.offset.y > 100 || info.velocity.y > 500) {
setIsAgendaOpen(false);
}
};
const selectedDayEvents = getDayEvents(selectedDate);
return (
<div className="max-w-7xl mx-auto p-4 md:p-8 flex flex-col h-screen max-h-screen relative z-10">
{/* Top Bar - Stacked on Mobile */}
<div className="flex flex-col md:flex-row justify-between items-center mb-4 gap-4">
{/* Status Bar */}
<div className="glass-panel rounded-full px-4 py-2 flex items-center justify-between w-full md:w-auto gap-4">
<div className="flex items-center gap-2 relative group cursor-pointer">
<div className={`w-8 h-8 rounded-full border-2 border-white/20 flex items-center justify-center ${currentUser === 'mom' ? 'bg-mom-primary' : 'bg-dad-primary'}`}>
{getStatusIcon(currentStatus)}
</div>
<span className="text-xs font-bold text-gray-300">{currentUser.toUpperCase()}</span>
<div className="absolute top-10 left-0 glass-panel rounded-xl p-2 hidden group-hover:block min-w-[150px] z-50 animate-fadeIn">
{statusOptions.map(opt => (
<button key={opt.id} onClick={() => onStatusChange(opt.id)} className="flex items-center gap-2 w-full p-2 hover:bg-white/10 rounded-lg text-left text-xs text-gray-300">
<opt.icon size={12} /> {opt.label}
</button>
))}
</div>
</div>
<div className="flex items-center gap-2 opacity-80">
<span className="text-xs font-bold text-gray-500">{partnerRole.toUpperCase()}</span>
<div className={`w-8 h-8 rounded-full border-2 border-white/10 flex items-center justify-center grayscale opacity-70`}>
{getStatusIcon(partnerStatus)}
</div>
</div>
</div>
{/* Title */}
<div className="flex items-end justify-center">
<Candle />
<div className="text-center">
<h1 className="font-magical text-3xl md:text-5xl text-transparent bg-clip-text bg-gradient-to-r from-purple-300 via-pink-300 to-blue-300 text-glow">
Celestial
</h1>
</div>
<Candle />
</div>
{/* Tools */}
<div className="flex gap-2">
<Button variant="ghost" onClick={() => setIsPulseOpen(true)} className={`!p-2 rounded-full text-pink-400 hover:text-white hover:bg-pink-900/30`}>
<Activity size={20} className="animate-pulse" />
</Button>
<Button variant="ghost" onClick={onOpenSettings} className="!p-2 rounded-full text-gray-400 hover:text-white"><SettingsIcon size={20} /></Button>
<Button variant="ghost" onClick={onOpenAssistant} className="!p-2 rounded-full text-blue-300 hover:text-white"><Bot size={20} /></Button>
<Button variant="ghost" onClick={onLogout} className="!p-2 rounded-full text-red-400 hover:text-red-200"><LogOut size={20} /></Button>
</div>
</div>
{/* Main Glass Panel */}
<div className={`flex-1 flex flex-col glass-panel rounded-[2rem] p-4 md:p-6 shadow-2xl relative overflow-hidden border transition-colors duration-500 ${isEditMode ? 'border-pink-500/40 bg-pink-900/10' : 'border-purple-500/10'}`}>
{/* Calendar Navigation */}
<div className="flex justify-between items-center mb-4">
<button onClick={() => changeMonth(-1)} className="p-2 hover:bg-white/10 rounded-full text-purple-200"><ChevronLeft /></button>
<h2 className="text-xl font-magical font-bold text-white cursor-pointer hover:text-purple-300 transition-colors" onClick={() => setCurrentDate(new Date())}>
{format(currentDate, 'MMMM yyyy')}
</h2>
<button onClick={() => changeMonth(1)} className="p-2 hover:bg-white/10 rounded-full text-purple-200"><ChevronRight /></button>
</div>
{/* Oracle Popup */}
<AnimatePresence>
{showOracle && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mb-4 bg-indigo-950/50 border border-indigo-500/30 rounded-2xl overflow-hidden"
>
<div className="p-4">
<h3 className="text-purple-300 font-magical text-sm mb-2 flex items-center gap-2">
<Sparkles size={14} /> The Stars Suggest...
</h3>
{isConsulting ? (
<p className="text-xs text-gray-400 animate-pulse">Gazing into the void...</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
{oracleWisdom.map((wis, i) => (
<div key={i} className="bg-white/5 p-3 rounded-xl text-xs text-indigo-200 border border-white/5 hover:bg-white/10 transition-colors cursor-pointer" onClick={() => onAddEvent()}>
{wis}
</div>
))}
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
{/* Days Header */}
<div className="grid grid-cols-7 mb-2 text-center">
{['S','M','T','W','T','F','S'].map(d => (
<div key={d} className="font-magical text-purple-400/60 text-xs py-1">{d}</div>
))}
</div>
{/* Calendar Grid */}
<div className="flex-1 relative">
<AnimatePresence initial={false} custom={direction} mode="wait">
<motion.div
key={currentDate.toISOString()}
custom={direction}
variants={{
enter: (d) => ({ x: d > 0 ? 50 : -50, opacity: 0 }),
center: { x: 0, opacity: 1 },
exit: (d) => ({ x: d > 0 ? -50 : 50, opacity: 0 })
}}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.3 }}
className="grid grid-cols-7 grid-rows-5 h-full gap-1 md:gap-2 pb-20 md:pb-0"
>
{days.map((day) => {
const dayEvents = getDayEvents(day);
const isCurrentMonth = isSameMonth(day, currentDate);
const isSelected = isSameDay(day, selectedDate);
const isTodayDate = isToday(day);
const dayKey = day.toISOString();
const isDragOver = dragOverDate === dayKey;
return (
<div
key={dayKey}
onClick={() => handleDayClick(day)}
onDragOver={(e) => handleDragOver(e, dayKey)}
onDrop={(e) => handleDrop(e, day)}
className={`
relative p-1 md:p-2 rounded-xl border transition-all duration-200 flex flex-col items-center justify-start
${isSelected ? 'bg-white/10 border-purple-500/50' : 'border-transparent'}
${isDragOver ? 'bg-purple-500/30 border-purple-400 scale-105 z-10' : ''}
${isCurrentMonth ? 'opacity-100' : 'opacity-30'}
${isTodayDate ? 'shadow-[0_0_10px_rgba(168,85,247,0.3)] border-purple-500/30' : ''}
`}
>
<span className={`text-[10px] md:text-xs font-bold ${isTodayDate ? 'text-purple-300' : 'text-gray-400'}`}>
{format(day, 'd')}
</span>
{/* Desktop View: Text List */}
<div className="hidden md:flex flex-col w-full gap-1 mt-1 overflow-y-auto max-h-[80px] custom-scrollbar">
{dayEvents.map(ev => (
<div
key={ev.id}
draggable={isEditMode}
onDragStart={(e) => handleDragStart(e, ev.id)}
onClick={(e) => { e.stopPropagation(); onEditEvent(ev); }}
className={`
text-[10px] px-1 rounded truncate cursor-pointer hover:brightness-110
${ev.category === 'date' ? 'bg-red-900/60 text-red-200' :
ev.category === 'magic' ? 'bg-gradient-to-r from-purple-600 to-blue-600 text-white' :
'bg-gray-800/80 text-gray-300'}
`}
>
{ev.title}
</div>
))}
</div>
{/* Mobile View: Dots */}
<div className="flex md:hidden flex-wrap justify-center gap-1 mt-1">
{dayEvents.slice(0, 4).map(ev => (
<div
key={ev.id}
className={`w-1.5 h-1.5 rounded-full ${
ev.category === 'date' ? 'bg-red-500' :
ev.category === 'magic' ? 'bg-purple-400 shadow-[0_0_4px_#a855f7]' :
'bg-gray-500'
}`}
/>
))}
{dayEvents.length > 4 && <div className="w-1.5 h-1.5 rounded-full bg-white/50" />}
</div>
</div>
)
})}
</motion.div>
</AnimatePresence>
</div>
{/* Mobile Agenda Sheet (Replaces inline list) */}
<AnimatePresence>
{selectedDate && isAgendaOpen && (
<motion.div
initial={{ y: "100%" }}
animate={{ y: 0 }}
exit={{ y: "100%" }}
transition={{ type: "spring", damping: 25, stiffness: 200 }}
drag="y"
dragConstraints={{ top: 0 }}
dragElastic={0.05}
onDragEnd={handleSheetDragEnd}
className="fixed bottom-0 left-0 right-0 z-40 bg-[#0B0B15]/95 backdrop-blur-2xl rounded-t-[2rem] border-t border-white/10 shadow-[0_-10px_50px_rgba(0,0,0,0.8)] md:hidden flex flex-col h-[50vh]"
>
{/* Drag Handle */}
<div
className="w-full flex flex-col items-center justify-center pt-3 pb-1 cursor-grab active:cursor-grabbing touch-none"
onClick={() => setIsAgendaOpen(false)}
>
<div className="w-12 h-1.5 bg-gray-600/50 rounded-full mb-1"></div>
<ChevronDown size={14} className="text-gray-600/50" />
</div>
<div className="p-6 pt-2 flex-1 overflow-hidden flex flex-col">
{/* Sheet Header */}
<div className="flex justify-between items-center mb-4 shrink-0">
<div>
<h3 className="text-2xl font-magical text-white">{format(selectedDate, 'EEEE')}</h3>
<p className="text-sm text-purple-300 font-medium">{format(selectedDate, 'MMMM do')}</p>
</div>
<Button onClick={() => onAddEvent(selectedDate)} className="!p-0 w-10 h-10 rounded-full bg-gradient-to-tr from-purple-600 to-pink-600 shadow-lg shadow-purple-900/40">
<Plus size={20} className="text-white" />
</Button>
</div>
{/* Events List */}
<div className="space-y-3 overflow-y-auto pb-20 custom-scrollbar">
{selectedDayEvents.length === 0 ? (
<div className="flex flex-col items-center justify-center py-10 text-gray-500">
<Moon className="w-8 h-8 mb-2 opacity-20" />
<p className="text-sm italic">No cosmic events planned.</p>
</div>
) : (
selectedDayEvents.map(ev => (
<motion.div
key={ev.id}
layoutId={ev.id}
onClick={() => onEditEvent(ev)}
className="bg-white/5 border border-white/5 p-4 rounded-2xl flex items-center gap-4 active:scale-95 transition-transform shadow-sm"
>
<div className={`w-2 h-10 rounded-full ${
ev.category === 'date' ? 'bg-red-500 shadow-[0_0_10px_#ef4444]' :
ev.category === 'magic' ? 'bg-purple-500 shadow-[0_0_10px_#a855f7]' :
'bg-blue-500'
}`} />
<div className="flex-1 min-w-0">
<h4 className="font-bold text-lg text-white truncate">{ev.title}</h4>
<p className="text-xs text-gray-400 flex items-center gap-1">
{format(parseISO(ev.startTime), 'h:mm a')} <span className="w-1 h-1 bg-gray-600 rounded-full"/> {ev.category}
</p>
</div>
<div className="p-2 bg-white/5 rounded-full">
<ChevronRight size={16} className="text-gray-500" />
</div>
</motion.div>
))
)}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Edit/Trash Actions */}
<div className="absolute bottom-6 right-6 md:right-6 flex flex-col gap-3 items-center z-50 pointer-events-none">
<div className="pointer-events-auto flex flex-col gap-3">
<Button
onClick={() => setIsEditMode(!isEditMode)}
className={`!rounded-full w-12 h-12 !p-0 border shadow-lg transition-all ${isEditMode ? 'bg-pink-600 border-pink-400 animate-pulse' : 'bg-black/60 border-white/10 text-gray-400'}`}
>
<Wand2 size={20} className={isEditMode ? "text-white" : ""} />
</Button>
{/* FAB (Visible on Desktop OR when Mobile Agenda is closed) */}
<AnimatePresence>
{!isEditMode && (!isAgendaOpen || window.innerWidth >= 768) && (
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
<Button onClick={() => onAddEvent(selectedDate)} className="!rounded-full w-14 h-14 !p-0 bg-gradient-to-tr from-purple-600 to-blue-600 shadow-[0_0_20px_rgba(124,58,237,0.4)] border border-white/20">
<Plus className="text-white" size={28} />
</Button>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
{/* The Void (Trash) */}
<AnimatePresence>
{isEditMode && (
<motion.div
initial={{ y: 100 }} animate={{ y: 0 }} exit={{ y: 100 }}
className="absolute bottom-8 left-1/2 -translate-x-1/2 z-50"
>
<div
onDragOver={(e) => e.preventDefault()}
onDrop={handleTrashDrop}
className="w-20 h-20 md:w-24 md:h-24 rounded-full bg-black border-2 border-red-900 shadow-[0_0_40px_rgba(255,0,0,0.3)] flex items-center justify-center relative overflow-hidden group backdrop-blur-md"
>
<div className="absolute inset-0 bg-[conic-gradient(from_0deg,transparent_0deg,rgba(255,0,0,0.5)_360deg)] animate-spin-slow opacity-50"></div>
<Trash2 size={28} className="text-red-500/70 z-10 group-hover:text-red-200 md:w-8 md:h-8" />
<span className="absolute bottom-3 text-[8px] text-red-500 uppercase font-bold tracking-widest">Void</span>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
{/* Pulse Modal */}
<CosmicPulseModal
isOpen={isPulseOpen}
onClose={() => setIsPulseOpen(false)}
events={events}
settings={settings}
/>
</div>
);
};

23
components/Candle.tsx Normal file
View File

@@ -0,0 +1,23 @@
import React from 'react';
export const Candle: React.FC = () => {
return (
<div className="relative w-8 h-24 mx-2">
{/* Flame */}
<div className="absolute -top-6 left-1/2 -translate-x-1/2 w-4 h-8 candle-flame animate-flicker z-20 shadow-[0_0_20px_rgba(255,165,0,0.8)]"></div>
{/* Wick */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-0.5 h-2 bg-black z-10 opacity-50"></div>
{/* Candle Body */}
<div className="absolute top-2 w-full h-full bg-gradient-to-b from-purple-100 to-purple-300 rounded-t-lg shadow-inner">
{/* Drip */}
<div className="absolute top-0 left-0 w-full h-4 bg-purple-100 rounded-t-lg blur-[1px]"></div>
<div className="absolute top-2 left-1 w-2 h-6 bg-purple-100 rounded-b-full opacity-80 shadow-sm"></div>
</div>
{/* Glow Base */}
<div className="absolute -bottom-2 left-1/2 -translate-x-1/2 w-16 h-4 bg-purple-500/20 blur-xl rounded-full"></div>
</div>
);
};

View File

@@ -0,0 +1,173 @@
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Activity, Heart, AlertTriangle, Zap, MessageCircle, Moon, Cloud } from 'lucide-react';
import { AppSettings, CalendarEvent, PulseAnalysis } from '../types';
import { analyzeRelationshipPulse, interpretDream } from '../services/aiService';
import { Button } from './Button';
interface CosmicPulseModalProps {
isOpen: boolean;
onClose: () => void;
events: CalendarEvent[];
settings: AppSettings;
}
export const CosmicPulseModal: React.FC<CosmicPulseModalProps> = ({ isOpen, onClose, events, settings }) => {
const [analysis, setAnalysis] = useState<PulseAnalysis | null>(null);
const [loading, setLoading] = useState(false);
const [dreamText, setDreamText] = useState('');
const [dreamResult, setDreamResult] = useState('');
const [dreamLoading, setDreamLoading] = useState(false);
useEffect(() => {
if (isOpen) {
setLoading(true);
analyzeRelationshipPulse(events, settings).then(res => {
setAnalysis(res);
setLoading(false);
});
} else {
// Reset state when closed
setAnalysis(null);
setDreamResult('');
setDreamText('');
}
}, [isOpen, events, settings]);
const handleInterpretDream = async () => {
if (!dreamText) return;
setDreamLoading(true);
const res = await interpretDream(dreamText, settings);
setDreamResult(res);
setDreamLoading(false);
};
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/80 backdrop-blur-md z-50"
onClick={onClose}
/>
<motion.div
initial={{ scale: 0.8, opacity: 0, y: 50 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.8, opacity: 0, y: 50 }}
className="fixed inset-0 flex items-center justify-center z-50 pointer-events-none p-4"
>
<div className="glass-panel w-full max-w-lg rounded-3xl overflow-hidden pointer-events-auto flex flex-col max-h-[90vh] border border-pink-500/30 shadow-[0_0_60px_rgba(236,72,153,0.3)]">
{/* Header */}
<div className="p-6 border-b border-white/10 flex justify-between items-center bg-black/40">
<h2 className="text-2xl font-magical text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-purple-400 flex items-center gap-3">
<Activity className="animate-pulse text-pink-500" /> Cosmic Pulse
</h2>
<button onClick={onClose} className="p-2 hover:bg-white/10 rounded-full transition-colors text-gray-400">
<X size={24} />
</button>
</div>
<div className="flex-1 overflow-y-auto p-6 space-y-6 custom-scrollbar">
{loading ? (
<div className="flex flex-col items-center justify-center py-12 gap-4">
<div className="relative">
<div className="w-16 h-16 border-4 border-pink-500/30 border-t-pink-500 rounded-full animate-spin"></div>
<div className="absolute inset-0 flex items-center justify-center">
<Activity size={24} className="text-pink-500 animate-pulse" />
</div>
</div>
<p className="text-pink-300 font-magical animate-pulse">Analyzing Relationship Rythm...</p>
<p className="text-xs text-gray-500">Using {settings.aiProvider === 'ollama' ? 'Local Spirit (Ollama)' : 'Cloud Mind (Gemini)'}</p>
</div>
) : analysis ? (
<>
{/* Harmony Score */}
<div className="bg-gradient-to-br from-purple-900/50 to-pink-900/50 p-6 rounded-3xl border border-pink-500/20 text-center relative overflow-hidden">
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/stardust.png')] opacity-20"></div>
<p className="text-pink-200 text-sm font-bold uppercase tracking-widest mb-2">Harmony Score</p>
<div className="text-6xl font-black text-white drop-shadow-[0_0_15px_rgba(236,72,153,0.5)]">
{analysis.harmonyScore}
</div>
<div className="mt-2 inline-block px-4 py-1 rounded-full bg-white/10 text-sm font-bold text-pink-200 border border-white/10">
Current Vibe: {analysis.vibe}
</div>
</div>
{/* Alerts */}
{analysis.burnoutWarning && (
<motion.div initial={{ x: -20, opacity: 0 }} animate={{ x: 0, opacity: 1 }} className="bg-red-900/30 border border-red-500/30 p-4 rounded-2xl flex items-start gap-3">
<AlertTriangle className="text-red-400 shrink-0" />
<div>
<h4 className="font-bold text-red-200 text-sm">Burnout Warning</h4>
<p className="text-xs text-red-300/80 mt-1">{analysis.burnoutWarning}</p>
</div>
</motion.div>
)}
{/* Conflicts */}
{analysis.upcomingConflicts.length > 0 && (
<div className="space-y-2">
<h3 className="text-sm font-bold text-gray-400 uppercase tracking-wider flex items-center gap-2">
<Zap size={14} /> Friction Points
</h3>
{analysis.upcomingConflicts.map((c, i) => (
<div key={i} className="bg-orange-900/20 border border-orange-500/20 p-3 rounded-xl text-xs text-orange-200">
{c}
</div>
))}
</div>
)}
{/* Smart Nudges */}
<div className="space-y-3">
<h3 className="text-sm font-bold text-gray-400 uppercase tracking-wider flex items-center gap-2">
<Heart size={14} /> Smart Nudges
</h3>
{analysis.smartNudges.map((nudge, i) => (
<div key={i} className="bg-gradient-to-r from-blue-900/30 to-purple-900/30 border border-blue-500/20 p-3 rounded-xl flex items-center gap-3">
<MessageCircle size={16} className="text-blue-300" />
<p className="text-sm text-blue-100 italic">"{nudge}"</p>
</div>
))}
</div>
</>
) : null}
{/* Dream Journal Section - Always visible */}
<div className="border-t border-white/10 pt-6">
<h3 className="text-sm font-bold text-gray-400 uppercase tracking-wider flex items-center gap-2 mb-3">
<Moon size={14} /> Dream Journal
</h3>
<div className="bg-indigo-950/40 rounded-2xl p-4 border border-indigo-500/20">
<textarea
value={dreamText}
onChange={(e) => setDreamText(e.target.value)}
placeholder="I dreamt I was flying over a grocery store made of clouds..."
className="w-full bg-black/30 border-0 rounded-xl p-3 text-sm text-indigo-100 placeholder-indigo-400/50 resize-none h-20 focus:ring-1 focus:ring-indigo-500"
/>
<div className="flex justify-between items-center mt-3">
<span className="text-[10px] text-gray-500">AI Interpreter Ready</span>
<Button size="sm" onClick={handleInterpretDream} disabled={!dreamText || dreamLoading} className="text-xs py-1 px-3 bg-indigo-600 hover:bg-indigo-500">
{dreamLoading ? <Cloud className="animate-bounce" size={14}/> : 'Interpret'}
</Button>
</div>
{dreamResult && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="mt-4 p-3 bg-white/5 rounded-xl border border-white/5">
<p className="text-xs text-purple-200 italic"> {dreamResult}</p>
</motion.div>
)}
</div>
</div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
};

308
components/EventModal.tsx Normal file
View 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>
);
};

76
components/FairyDust.tsx Normal file
View File

@@ -0,0 +1,76 @@
import React, { useEffect, useState } from 'react';
interface Point {
x: number;
y: number;
id: number;
size: number;
color: string;
velocity: { x: number; y: number };
life: number;
}
export const FairyDust: React.FC = () => {
const [points, setPoints] = useState<Point[]>([]);
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => {
const colors = ['#FF00CC', '#3333FF', '#7000FF', '#FFFFFF', '#00FFFF'];
const newPoint: Point = {
x: e.clientX,
y: e.clientY,
id: Date.now() + Math.random(),
size: Math.random() * 4 + 2,
color: colors[Math.floor(Math.random() * colors.length)],
velocity: {
x: (Math.random() - 0.5) * 2,
y: (Math.random() - 0.5) * 2
},
life: 1.0
};
setPoints(prev => [...prev.slice(-40), newPoint]); // Limit trail length
};
window.addEventListener('mousemove', handleMouseMove);
return () => window.removeEventListener('mousemove', handleMouseMove);
}, []);
useEffect(() => {
const interval = setInterval(() => {
setPoints(prev => prev
.map(p => ({
...p,
x: p.x + p.velocity.x,
y: p.y + p.velocity.y,
life: p.life - 0.05
}))
.filter(p => p.life > 0)
);
}, 16); // 60fps
return () => clearInterval(interval);
}, []);
return (
<div className="fixed inset-0 pointer-events-none z-[9999]">
{points.map(point => (
<div
key={point.id}
style={{
position: 'absolute',
left: point.x,
top: point.y,
width: point.size,
height: point.size,
backgroundColor: point.color,
borderRadius: '50%',
opacity: point.life,
transform: `scale(${point.life})`,
boxShadow: `0 0 ${point.size * 2}px ${point.color}`,
transition: 'opacity 0.1s linear'
}}
/>
))}
</div>
);
};

106
components/LoginScreen.tsx Normal file
View File

@@ -0,0 +1,106 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { UserRole } from '../types';
import { Button } from './Button';
import { Heart } from 'lucide-react';
interface LoginScreenProps {
onLogin: (role: UserRole) => void;
}
export const LoginScreen: React.FC<LoginScreenProps> = ({ onLogin }) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleLogin = (e: React.FormEvent) => {
e.preventDefault();
const user = username.toLowerCase().trim();
const pass = password.trim();
if (pass !== 'password') {
setError('Wrong password! Hint: password');
return;
}
if (user === 'mom') {
onLogin('mom');
} else if (user === 'dad') {
onLogin('dad');
} else {
setError('Username must be "mom" or "dad"');
}
};
return (
<div className="min-h-screen flex items-center justify-center p-4 bg-gray-900">
{/* Background decorative blobs */}
<div className="fixed top-0 left-0 w-64 h-64 bg-purple-600 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob"></div>
<div className="fixed top-0 right-0 w-64 h-64 bg-blue-600 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-2000"></div>
<div className="fixed -bottom-8 left-20 w-64 h-64 bg-pink-600 rounded-full mix-blend-multiply filter blur-3xl opacity-20 animate-blob animation-delay-4000"></div>
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.5 }}
className="glass-panel p-8 rounded-[2.5rem] shadow-2xl w-full max-w-sm border border-white/10 relative z-10"
>
<div className="text-center mb-8">
<div className="w-20 h-20 bg-gradient-to-tr from-pink-500 to-blue-500 rounded-full mx-auto mb-4 flex items-center justify-center shadow-lg shadow-purple-500/20">
<Heart className="text-white fill-white" size={40} />
</div>
<h1 className="text-3xl font-black text-white tracking-tight">Welcome Home</h1>
<p className="text-gray-400 font-medium mt-1">Shared Calendar App</p>
</div>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<input
type="text"
placeholder="Who are you? (mom/dad)"
value={username}
onChange={(e) => {
setUsername(e.target.value);
setError('');
}}
className="w-full px-6 py-4 rounded-2xl bg-gray-800 border-2 border-transparent focus:bg-gray-700 focus:border-blue-500 focus:ring-0 transition-all font-bold text-gray-200 placeholder-gray-500 outline-none"
/>
</div>
<div>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => {
setPassword(e.target.value);
setError('');
}}
className="w-full px-6 py-4 rounded-2xl bg-gray-800 border-2 border-transparent focus:bg-gray-700 focus:border-blue-500 focus:ring-0 transition-all font-bold text-gray-200 placeholder-gray-500 outline-none"
/>
</div>
{error && (
<motion.p
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
className="text-red-400 text-sm font-bold text-center bg-red-900/20 py-2 rounded-xl border border-red-900/50"
>
{error}
</motion.p>
)}
<Button
type="submit"
className="w-full bg-gradient-to-r from-mom-primary to-dad-primary hover:from-pink-400 hover:to-blue-400 shadow-lg shadow-purple-900/50 text-white border-0"
>
Enter House
</Button>
</form>
<p className="text-center text-xs text-gray-500 mt-8 font-semibold">
Secure Shared Simple
</p>
</motion.div>
</div>
);
};

View File

@@ -0,0 +1,185 @@
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Save, Server, Palette, Cpu, BookOpen, Star, Cat, Cloud, Database } from 'lucide-react';
import { AppSettings, ZodiacSign, Familiar, AIProvider } from '../types';
import { Button } from './Button';
interface SettingsModalProps {
isOpen: boolean;
onClose: () => void;
settings: AppSettings;
onSave: (settings: AppSettings) => void;
}
const zodiacSigns: ZodiacSign[] = ['Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces'];
const familiars: Familiar[] = ['Cat', 'Owl', 'Toad', 'Bat', 'Crow', 'Wolf', 'None'];
export const SettingsModal: React.FC<SettingsModalProps> = ({ isOpen, onClose, settings, onSave }) => {
const [localSettings, setLocalSettings] = useState<AppSettings>(settings);
const [activeTab, setActiveTab] = useState<'general' | 'identity' | 'ai'>('general');
const handleSave = () => {
onSave(localSettings);
onClose();
};
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50"
onClick={onClose}
/>
<motion.div
initial={{ scale: 0.9, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.9, opacity: 0 }}
className="fixed inset-0 flex items-center justify-center z-50 pointer-events-none"
>
<div className="glass-panel w-full max-w-lg mx-4 rounded-3xl overflow-hidden pointer-events-auto flex flex-col max-h-[85vh] border border-purple-500/20 shadow-[0_0_50px_rgba(112,0,255,0.2)]">
{/* Header */}
<div className="p-6 border-b border-white/10 flex justify-between items-center bg-black/40">
<h2 className="text-2xl font-magical text-purple-300 flex items-center gap-2">
<BookOpen size={24} /> The Grimoire
</h2>
<button onClick={onClose} className="p-2 hover:bg-white/10 rounded-full transition-colors">
<X size={24} className="text-gray-400" />
</button>
</div>
{/* Tabs */}
<div className="flex p-2 gap-2 bg-black/20 overflow-x-auto">
<button onClick={() => setActiveTab('general')} className={`flex-1 py-2 px-3 whitespace-nowrap rounded-xl text-xs font-bold transition-all ${activeTab === 'general' ? 'bg-purple-900/40 text-purple-200 border border-purple-500/30' : 'text-gray-500'}`}>
Runes & Colors
</button>
<button onClick={() => setActiveTab('identity')} className={`flex-1 py-2 px-3 whitespace-nowrap rounded-xl text-xs font-bold transition-all ${activeTab === 'identity' ? 'bg-pink-900/40 text-pink-200 border border-pink-500/30' : 'text-gray-500'}`}>
Cosmic Identity
</button>
<button onClick={() => setActiveTab('ai')} className={`flex-1 py-2 px-3 whitespace-nowrap rounded-xl text-xs font-bold transition-all ${activeTab === 'ai' ? 'bg-blue-900/40 text-blue-200 border border-blue-500/30' : 'text-gray-500'}`}>
Brain & Spirit
</button>
</div>
<div className="p-6 overflow-y-auto custom-scrollbar space-y-6">
{activeTab === 'general' ? (
<>
<div>
<h3 className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3 flex items-center gap-2">
<Palette size={14} /> Aura Colors
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="bg-white/5 p-4 rounded-2xl border border-white/5">
<label className="text-xs font-semibold text-gray-400 block mb-2">Mom's Aura</label>
<input type="color" value={localSettings.momColor} onChange={(e) => setLocalSettings({...localSettings, momColor: e.target.value})} className="w-full h-10 rounded bg-transparent cursor-pointer" />
</div>
<div className="bg-white/5 p-4 rounded-2xl border border-white/5">
<label className="text-xs font-semibold text-gray-400 block mb-2">Dad's Aura</label>
<input type="color" value={localSettings.dadColor} onChange={(e) => setLocalSettings({...localSettings, dadColor: e.target.value})} className="w-full h-10 rounded bg-transparent cursor-pointer" />
</div>
</div>
</div>
<div>
<h3 className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3 flex items-center gap-2">
<Server size={14} /> Telepathy Link (SSE)
</h3>
<div className="bg-white/5 p-4 rounded-2xl border border-white/5 space-y-4">
<label className="flex items-center gap-3 cursor-pointer">
<input type="checkbox" checked={localSettings.enableMcp} onChange={(e) => setLocalSettings({...localSettings, enableMcp: e.target.checked})} className="w-4 h-4 rounded bg-gray-800 border-gray-600 text-purple-500" />
<span className="text-sm font-medium text-gray-300">Enable Neural Sync</span>
</label>
{localSettings.enableMcp && (
<input type="text" value={localSettings.mcpServerUrl} onChange={(e) => setLocalSettings({...localSettings, mcpServerUrl: e.target.value})} className="w-full p-3 rounded-xl bg-black/50 border border-white/10 text-gray-300 text-sm font-mono focus:border-purple-500 outline-none" placeholder="http://server..." />
)}
</div>
</div>
</>
) : activeTab === 'identity' ? (
<>
<div>
<h3 className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3 flex items-center gap-2">
<Star size={14} /> Star Signs
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="bg-white/5 p-4 rounded-2xl border border-white/5">
<label className="text-xs font-semibold text-gray-400 block mb-2">Mom</label>
<select value={localSettings.momZodiac} onChange={(e) => setLocalSettings({...localSettings, momZodiac: e.target.value as ZodiacSign})} className="w-full bg-black/50 text-gray-200 text-sm rounded-lg p-2 border border-white/10 outline-none">
{zodiacSigns.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div className="bg-white/5 p-4 rounded-2xl border border-white/5">
<label className="text-xs font-semibold text-gray-400 block mb-2">Dad</label>
<select value={localSettings.dadZodiac} onChange={(e) => setLocalSettings({...localSettings, dadZodiac: e.target.value as ZodiacSign})} className="w-full bg-black/50 text-gray-200 text-sm rounded-lg p-2 border border-white/10 outline-none">
{zodiacSigns.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
</div>
</div>
<div>
<h3 className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3 flex items-center gap-2">
<Cat size={14} /> House Familiar
</h3>
<div className="bg-white/5 p-4 rounded-2xl border border-white/5">
<select value={localSettings.familiar} onChange={(e) => setLocalSettings({...localSettings, familiar: e.target.value as Familiar})} className="w-full bg-black/50 text-gray-200 text-sm rounded-lg p-2 border border-white/10 outline-none">
{familiars.map(f => <option key={f} value={f}>{f}</option>)}
</select>
<p className="text-[10px] text-gray-500 mt-2">Your spiritual guardian.</p>
</div>
</div>
</>
) : (
<>
<div>
<h3 className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-3 flex items-center gap-2">
<Cpu size={14} /> Intelligence Source
</h3>
<div className="flex bg-black/50 p-1 rounded-xl border border-white/10 mb-4">
<button
onClick={() => setLocalSettings({...localSettings, aiProvider: 'gemini'})}
className={`flex-1 py-2 rounded-lg text-sm font-bold flex items-center justify-center gap-2 transition-all ${localSettings.aiProvider === 'gemini' ? 'bg-purple-600 text-white shadow-lg' : 'text-gray-400 hover:text-white'}`}
>
<Cloud size={14} /> Gemini
</button>
<button
onClick={() => setLocalSettings({...localSettings, aiProvider: 'ollama'})}
className={`flex-1 py-2 rounded-lg text-sm font-bold flex items-center justify-center gap-2 transition-all ${localSettings.aiProvider === 'ollama' ? 'bg-blue-600 text-white shadow-lg' : 'text-gray-400 hover:text-white'}`}
>
<Database size={14} /> Ollama
</button>
</div>
</div>
{localSettings.aiProvider === 'ollama' && (
<div className="space-y-4 animate-fadeIn">
<div className="bg-blue-900/10 p-4 rounded-2xl border border-blue-500/20">
<h4 className="text-blue-300 font-bold text-sm flex items-center gap-2"><Cpu size={16} /> Local Spirit</h4>
<p className="text-blue-400/60 text-xs mt-1">Ensure Ollama is running with CORS enabled.</p>
</div>
<div>
<label className="text-xs font-semibold text-gray-400 block mb-1">URL</label>
<input type="text" value={localSettings.ollamaUrl} onChange={(e) => setLocalSettings({...localSettings, ollamaUrl: e.target.value})} className="w-full p-3 rounded-xl bg-black/50 border border-white/10 text-gray-300 text-sm font-mono focus:border-blue-500 outline-none" placeholder="http://localhost:11434" />
</div>
<div>
<label className="text-xs font-semibold text-gray-400 block mb-1">Model</label>
<input type="text" value={localSettings.ollamaModel} onChange={(e) => setLocalSettings({...localSettings, ollamaModel: e.target.value})} className="w-full p-3 rounded-xl bg-black/50 border border-white/10 text-gray-300 text-sm font-mono focus:border-blue-500 outline-none" placeholder="llama3" />
</div>
</div>
)}
</>
)}
</div>
<div className="p-6 border-t border-white/10 bg-black/40">
<Button onClick={handleSave} className="w-full bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500 text-white shadow-lg shadow-purple-900/50">
<Save size={18} /> Inscribe Changes
</Button>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
};

View File

@@ -0,0 +1,54 @@
import React from 'react';
export const SpaceBackground: React.FC = () => {
return (
<div className="fixed inset-0 z-[-1] overflow-hidden bg-[#050510]">
{/* Deep Space Gradients */}
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_50%,rgba(17,24,39,0)_0%,#050510_100%)]" />
<div className="absolute top-[-20%] left-[-20%] w-[80%] h-[80%] bg-purple-900/10 rounded-full blur-[150px] animate-pulse" />
<div className="absolute bottom-[-20%] right-[-20%] w-[80%] h-[80%] bg-blue-900/10 rounded-full blur-[150px] animate-pulse" style={{ animationDelay: '3s' }} />
{/* Stars */}
<div className="absolute inset-0 opacity-60">
{[...Array(80)].map((_, i) => (
<div
key={i}
className="absolute rounded-full bg-white animate-twinkle"
style={{
top: `${Math.random() * 100}%`,
left: `${Math.random() * 100}%`,
width: `${Math.random() * 2 + 1}px`,
height: `${Math.random() * 2 + 1}px`,
animationDelay: `${Math.random() * 5}s`,
boxShadow: `0 0 ${Math.random() * 4}px rgba(255, 255, 255, 0.8)`
}}
/>
))}
</div>
{/* Shooting Stars */}
<div className="absolute top-[10%] left-[80%] w-[2px] h-[2px] bg-white shadow-[0_0_20px_2px_white] animate-shoot opacity-0" />
<div className="absolute top-[30%] left-[90%] w-[2px] h-[2px] bg-white shadow-[0_0_20px_2px_white] animate-shoot opacity-0" style={{ animationDelay: '5s' }} />
<div className="absolute top-[5%] left-[60%] w-[3px] h-[3px] bg-cyan-200 shadow-[0_0_20px_2px_cyan] animate-shoot opacity-0" style={{ animationDelay: '12s', animationDuration: '6s' }} />
{/* Nebula Blobs - More intense */}
<div className="absolute top-1/4 left-1/4 w-[400px] h-[400px] bg-pink-900/10 rounded-full blur-[100px] animate-float" style={{ animationDuration: '20s' }} />
<div className="absolute bottom-1/4 right-1/4 w-[500px] h-[500px] bg-indigo-900/10 rounded-full blur-[120px] animate-float" style={{ animationDelay: '2s', animationDuration: '25s' }} />
{/* Orbiting Planets - Detailed */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[900px] h-[900px] border border-white/5 rounded-full animate-spin-slow">
{/* Planet 1 */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-gradient-to-br from-blue-400 to-blue-900 shadow-[0_0_30px_rgba(59,130,246,0.4)]">
<div className="absolute inset-0 rounded-full bg-black/20" style={{ clipPath: 'inset(0 0 50% 0)' }}></div>
</div>
</div>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] border border-white/5 rounded-full animate-spin-slow" style={{ animationDirection: 'reverse', animationDuration: '90s' }}>
{/* Planet 2 */}
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-1/2 w-8 h-8 rounded-full bg-gradient-to-br from-purple-400 to-purple-900 shadow-[0_0_20px_rgba(168,85,247,0.4)]"></div>
</div>
</div>
);
};