458
components/CalendarView.tsx
Normal file
458
components/CalendarView.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user