Skip to content

Commit 5f83899

Browse files
authored
Merge pull request #381 from walterthesmart/fix/all-issues-377-378-379-380
Resolve Issues #377, #378, #379, #380: Keyboard, Session, Virtualization & DND
2 parents e258acf + 97cf80e commit 5f83899

6 files changed

Lines changed: 756 additions & 0 deletions

File tree

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"use client";
2+
3+
import React, { useState, useEffect, useCallback, useRef } from 'react';
4+
import { motion, AnimatePresence } from 'motion/react';
5+
import { AlertCircle, Clock, LogOut, RefreshCcw, ShieldAlert } from 'lucide-react';
6+
7+
interface SessionTimeoutHandlerProps {
8+
timeoutMinutes?: number;
9+
warningMinutes?: number;
10+
onLogout?: () => void;
11+
onRefresh?: () => Promise<void>;
12+
className?: string;
13+
}
14+
15+
const SessionTimeoutHandler: React.FC<SessionTimeoutHandlerProps> = ({
16+
timeoutMinutes = 15,
17+
warningMinutes = 2,
18+
onLogout,
19+
onRefresh,
20+
className = '',
21+
}) => {
22+
const [isWarningOpen, setIsWarningOpen] = useState(false);
23+
const [secondsRemaining, setSecondsRemaining] = useState(0);
24+
const lastActivityRef = useRef<number>(Date.now());
25+
const timerRef = useRef<NodeJS.Timeout | null>(null);
26+
27+
const logout = useCallback(() => {
28+
setIsWarningOpen(false);
29+
if (onLogout) {
30+
onLogout();
31+
} else {
32+
// Default logout logic (e.g., clear localStorage, redirect)
33+
window.location.href = '/login';
34+
}
35+
}, [onLogout]);
36+
37+
const refreshSession = useCallback(async () => {
38+
try {
39+
if (onRefresh) {
40+
await onRefresh();
41+
}
42+
setIsWarningOpen(false);
43+
lastActivityRef.current = Date.now();
44+
} catch (error) {
45+
console.error('Failed to refresh session:', error);
46+
}
47+
}, [onRefresh]);
48+
49+
useEffect(() => {
50+
const handleActivity = () => {
51+
lastActivityRef.current = Date.now();
52+
if (isWarningOpen) {
53+
// Optionally auto-refresh if they become active during warning
54+
// refreshSession();
55+
}
56+
};
57+
58+
const activityEvents = ['mousedown', 'keydown', 'touchstart', 'scroll', 'click'];
59+
activityEvents.forEach(event => document.addEventListener(event, handleActivity));
60+
61+
const checkInactivity = () => {
62+
const now = Date.now();
63+
const inactiveMs = now - lastActivityRef.current;
64+
const timeoutMs = timeoutMinutes * 60 * 1000;
65+
const warningMs = (timeoutMinutes - warningMinutes) * 60 * 1000;
66+
67+
if (inactiveMs >= timeoutMs) {
68+
logout();
69+
} else if (inactiveMs >= warningMs && !isWarningOpen) {
70+
setIsWarningOpen(true);
71+
setSecondsRemaining(Math.floor((timeoutMs - inactiveMs) / 1000));
72+
} else if (isWarningOpen) {
73+
setSecondsRemaining(Math.floor((timeoutMs - inactiveMs) / 1000));
74+
}
75+
};
76+
77+
timerRef.current = setInterval(checkInactivity, 1000);
78+
79+
return () => {
80+
activityEvents.forEach(event => document.removeEventListener(event, handleActivity));
81+
if (timerRef.current) clearInterval(timerRef.current);
82+
};
83+
}, [timeoutMinutes, warningMinutes, isWarningOpen, logout]);
84+
85+
const formatTime = (seconds: number) => {
86+
const mins = Math.floor(seconds / 60);
87+
const secs = seconds % 60;
88+
return `${mins}:${secs.toString().padStart(2, '0')}`;
89+
};
90+
91+
return (
92+
<AnimatePresence>
93+
{isWarningOpen && (
94+
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm">
95+
<motion.div
96+
initial={{ opacity: 0, scale: 0.9, y: 20 }}
97+
animate={{ opacity: 1, scale: 1, y: 0 }}
98+
exit={{ opacity: 0, scale: 0.9, y: 20 }}
99+
className={`w-full max-w-md bg-white dark:bg-gray-900 rounded-3xl shadow-2xl border border-gray-100 dark:border-gray-800 overflow-hidden ${className}`}
100+
>
101+
<div className="p-1 bg-amber-500" />
102+
103+
<div className="p-8">
104+
<div className="flex items-center gap-4 mb-6">
105+
<div className="p-3 bg-amber-100 dark:bg-amber-900/30 rounded-2xl">
106+
<ShieldAlert className="w-8 h-8 text-amber-600 dark:text-amber-400" />
107+
</div>
108+
<div>
109+
<h3 className="text-xl font-bold text-gray-900 dark:text-white">Session Security</h3>
110+
<p className="text-sm text-gray-500 dark:text-gray-400">Inactivity detected</p>
111+
</div>
112+
</div>
113+
114+
<div className="bg-gray-50 dark:bg-gray-800/50 rounded-2xl p-6 mb-8 text-center ring-1 ring-inset ring-gray-100 dark:ring-gray-700">
115+
<div className="flex justify-center mb-2">
116+
<Clock className="w-5 h-5 text-indigo-500 animate-pulse" />
117+
</div>
118+
<div className="text-4xl font-black text-gray-900 dark:text-white font-mono tracking-tighter">
119+
{formatTime(secondsRemaining)}
120+
</div>
121+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2 uppercase tracking-widest font-bold">Time Remaining</p>
122+
</div>
123+
124+
<p className="text-sm text-gray-600 dark:text-gray-300 mb-8 leading-relaxed">
125+
For your security, you will be automatically logged out soon. Would you like to stay connected?
126+
</p>
127+
128+
<div className="grid grid-cols-2 gap-3">
129+
<motion.button
130+
whileHover={{ scale: 1.02 }}
131+
whileTap={{ scale: 0.98 }}
132+
onClick={logout}
133+
className="flex items-center justify-center gap-2 px-4 py-3 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-200 rounded-xl font-bold hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
134+
>
135+
<LogOut className="w-4 h-4" />
136+
Logout
137+
</motion.button>
138+
139+
<motion.button
140+
whileHover={{ scale: 1.02 }}
141+
whileTap={{ scale: 0.98 }}
142+
onClick={refreshSession}
143+
className="flex items-center justify-center gap-2 px-4 py-3 bg-indigo-600 text-white rounded-xl font-bold shadow-lg shadow-indigo-500/25 hover:bg-indigo-700 transition-colors"
144+
>
145+
<RefreshCcw className="w-4 h-4" />
146+
Stay Active
147+
</motion.button>
148+
</div>
149+
</div>
150+
151+
<div className="px-8 py-4 bg-gray-50 dark:bg-gray-800/30 border-t border-gray-100 dark:border-gray-800 flex items-center gap-2">
152+
<AlertCircle className="w-4 h-4 text-gray-400" />
153+
<span className="text-[10px] text-gray-400 uppercase font-bold tracking-wider">Gatherraa Secure Session Manager</span>
154+
</div>
155+
</motion.div>
156+
</div>
157+
)}
158+
</AnimatePresence>
159+
);
160+
};
161+
162+
export { SessionTimeoutHandler };
163+
export type { SessionTimeoutHandlerProps };
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
"use client";
2+
3+
import React, { useState, useCallback, useMemo } from 'react';
4+
import { motion, Reorder, AnimatePresence } from 'motion/react';
5+
import { GripVertical, Clock, MapPin, Plus, Trash2, Edit3, Save, RotateCcw } from 'lucide-react';
6+
7+
interface Session {
8+
id: string;
9+
title: string;
10+
time: string;
11+
duration: string;
12+
location: string;
13+
speaker: string;
14+
type: 'workshop' | 'keynote' | 'break' | 'networking';
15+
}
16+
17+
interface ScheduleBuilderProps {
18+
initialSessions?: Session[];
19+
onSave?: (sessions: Session[]) => void;
20+
className?: string;
21+
}
22+
23+
const DEFAULT_SESSIONS: Session[] = [
24+
{ id: '1', title: 'Opening Ceremony', time: '09:00 AM', duration: '30 min', location: 'Main Stage', speaker: 'Alice Johnson', type: 'keynote' },
25+
{ id: '2', title: 'Blockchain Fundamentals', time: '10:00 AM', duration: '60 min', location: 'Room A', speaker: 'Bob Smith', type: 'workshop' },
26+
{ id: '3', title: 'Networking Lunch', time: '12:00 PM', duration: '60 min', location: 'Cafeteria', speaker: '-', type: 'networking' },
27+
{ id: '4', title: 'Future of DeFi', time: '01:30 PM', duration: '45 min', location: 'Main Stage', speaker: 'Charlie Brown', type: 'workshop' },
28+
];
29+
30+
const ScheduleBuilder: React.FC<ScheduleBuilderProps> = ({
31+
initialSessions = DEFAULT_SESSIONS,
32+
onSave,
33+
className = '',
34+
}) => {
35+
const [items, setItems] = useState<Session[]>(initialSessions);
36+
const [isEditing, setIsEditing] = useState(false);
37+
const [isHovering, setIsHovering] = useState<string | null>(null);
38+
39+
const handleReorder = (newOrder: Session[]) => {
40+
setItems(newOrder);
41+
};
42+
43+
const addSession = () => {
44+
const newSession: Session = {
45+
id: Math.random().toString(36).substr(2, 9),
46+
title: 'New Session',
47+
time: '12:00 PM',
48+
duration: '45 min',
49+
location: 'TBD',
50+
speaker: 'TBD',
51+
type: 'workshop',
52+
};
53+
setItems([...items, newSession]);
54+
};
55+
56+
const removeSession = (id: string) => {
57+
setItems(items.filter(item => item.id !== id));
58+
};
59+
60+
const saveChanges = () => {
61+
onSave?.(items);
62+
setIsEditing(false);
63+
};
64+
65+
const resetSchedule = () => {
66+
setItems(initialSessions);
67+
};
68+
69+
const getTypeColor = (type: Session['type']) => {
70+
switch (type) {
71+
case 'keynote': return 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400';
72+
case 'workshop': return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400';
73+
case 'break': return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400';
74+
case 'networking': return 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400';
75+
default: return 'bg-gray-100 text-gray-700 dark:bg-gray-800/30 dark:text-gray-400';
76+
}
77+
};
78+
79+
return (
80+
<div className={`max-w-4xl mx-auto w-full p-6 bg-white dark:bg-gray-900 rounded-3xl shadow-xl border border-gray-100 dark:border-gray-800 ${className}`}>
81+
{/* Header */}
82+
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8">
83+
<div>
84+
<h2 className="text-2xl font-extrabold text-gray-900 dark:text-white flex items-center gap-2">
85+
Schedule Builder
86+
<span className="px-2 py-1 bg-indigo-100 dark:bg-indigo-900/30 text-indigo-600 dark:text-indigo-400 text-xs font-bold rounded-lg uppercase tracking-wider">Editor</span>
87+
</h2>
88+
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Drag and drop to reorder sessions or add new ones.</p>
89+
</div>
90+
91+
<div className="flex items-center gap-2">
92+
<motion.button
93+
whileHover={{ scale: 1.05 }}
94+
whileTap={{ scale: 0.95 }}
95+
onClick={resetSchedule}
96+
className="p-2.5 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-white bg-gray-50 dark:bg-gray-800 rounded-xl transition-all"
97+
title="Reset to initial"
98+
>
99+
<RotateCcw className="w-5 h-5" />
100+
</motion.button>
101+
102+
<motion.button
103+
whileHover={{ scale: 1.05 }}
104+
whileTap={{ scale: 0.95 }}
105+
onClick={addSession}
106+
className="flex items-center gap-2 px-4 py-2.5 bg-indigo-50 text-indigo-600 dark:bg-indigo-900/10 dark:text-indigo-400 rounded-xl font-semibold border border-indigo-100 dark:border-indigo-800/50 hover:bg-indigo-100 dark:hover:bg-indigo-900/20 transition-all"
107+
>
108+
<Plus className="w-5 h-5" />
109+
Add Session
110+
</motion.button>
111+
112+
<motion.button
113+
whileHover={{ scale: 1.05 }}
114+
whileTap={{ scale: 0.95 }}
115+
onClick={saveChanges}
116+
className="flex items-center gap-2 px-6 py-2.5 bg-indigo-600 text-white rounded-xl font-semibold shadow-lg shadow-indigo-500/25 hover:bg-indigo-700 transition-all"
117+
>
118+
<Save className="w-5 h-5" />
119+
Save Changes
120+
</motion.button>
121+
</div>
122+
</div>
123+
124+
{/* Schedule List */}
125+
<div className="space-y-4">
126+
<Reorder.Group axis="y" values={items} onReorder={handleReorder} className="space-y-3">
127+
<AnimatePresence mode="popLayout">
128+
{items.map((session) => (
129+
<Reorder.Item
130+
key={session.id}
131+
value={session}
132+
initial={{ opacity: 0, scale: 0.95 }}
133+
animate={{ opacity: 1, scale: 1 }}
134+
exit={{ opacity: 0, scale: 0.9 }}
135+
transition={{ duration: 0.2 }}
136+
onHoverStart={() => setIsHovering(session.id)}
137+
onHoverEnd={() => setIsHovering(null)}
138+
className="relative group list-none"
139+
>
140+
<div className={`p-5 flex items-center gap-4 bg-gray-50 dark:bg-gray-800/50 border border-transparent rounded-2xl transition-all duration-300 ${isHovering === session.id ? 'border-indigo-300/40 dark:border-indigo-700/40 shadow-sm' : ''}`}>
141+
{/* Grip Handle */}
142+
<div className="cursor-grab active:cursor-grabbing text-gray-400 hover:text-indigo-500 transition-colors p-1">
143+
<GripVertical className="w-6 h-6" />
144+
</div>
145+
146+
{/* Content Container */}
147+
<div className="flex-1 min-w-0 grid grid-cols-1 md:grid-cols-12 gap-4 items-center">
148+
{/* Time & Session Type */}
149+
<div className="md:col-span-3">
150+
<div className="flex items-center gap-2 text-gray-900 dark:text-white font-bold text-lg mb-1">
151+
<Clock className="w-4 h-4 text-indigo-500" />
152+
{session.time}
153+
</div>
154+
<span className={`px-2 py-0.5 rounded-lg text-[10px] font-bold uppercase tracking-wider ${getTypeColor(session.type)}`}>
155+
{session.type}
156+
</span>
157+
</div>
158+
159+
{/* Title & Speaker */}
160+
<div className="md:col-span-5">
161+
<h3 className="font-bold text-gray-900 dark:text-white truncate group-hover:text-indigo-500 transition-colors">
162+
{session.title}
163+
</h3>
164+
<p className="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1.5 mt-0.5">
165+
<span className="font-medium text-gray-900 dark:text-gray-300">{session.speaker}</span>
166+
<span>{session.duration}</span>
167+
</p>
168+
</div>
169+
170+
{/* Location */}
171+
<div className="md:col-span-4 flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 truncate">
172+
<MapPin className="w-4 h-4 text-gray-400" />
173+
<span className="truncate">{session.location}</span>
174+
</div>
175+
</div>
176+
177+
{/* Actions */}
178+
<div className="flex items-center gap-1">
179+
<button className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-white dark:hover:bg-gray-700 rounded-lg transition-all" title="Edit Session">
180+
<Edit3 className="w-4 h-4" />
181+
</button>
182+
<button
183+
onClick={() => removeSession(session.id)}
184+
className="p-2 text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-all"
185+
title="Delete Session"
186+
>
187+
<Trash2 className="w-4 h-4" />
188+
</button>
189+
</div>
190+
</div>
191+
</Reorder.Item>
192+
))}
193+
</AnimatePresence>
194+
</Reorder.Group>
195+
196+
{items.length === 0 && (
197+
<motion.div
198+
initial={{ opacity: 0 }}
199+
animate={{ opacity: 1 }}
200+
className="flex flex-col items-center justify-center py-16 px-4 text-center bg-gray-50/50 dark:bg-gray-800/30 rounded-3xl border border-dashed border-gray-200 dark:border-gray-700"
201+
>
202+
<div className="w-16 h-16 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center mb-4">
203+
<Plus className="w-8 h-8 text-gray-300" />
204+
</div>
205+
<h3 className="text-lg font-bold text-gray-900 dark:text-white">No sessions yet</h3>
206+
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1 max-w-xs">Start building your event schedule by adding your first session.</p>
207+
<button
208+
onClick={addSession}
209+
className="mt-6 px-6 py-2.5 bg-indigo-600 text-white rounded-xl font-semibold shadow-md active:scale-95 transition-all"
210+
>
211+
Add First Session
212+
</button>
213+
</motion.div>
214+
)}
215+
</div>
216+
217+
{/* Footer Info */}
218+
<div className="mt-10 p-4 rounded-2xl bg-indigo-50/50 dark:bg-indigo-900/10 border border-indigo-100/50 dark:border-indigo-800/20 flex items-start gap-4">
219+
<div className="p-2 bg-indigo-100 dark:bg-indigo-900/30 rounded-xl">
220+
<GripVertical className="w-5 h-5 text-indigo-600 dark:text-indigo-400" />
221+
</div>
222+
<div>
223+
<h4 className="text-sm font-bold text-indigo-900 dark:text-indigo-100">Pro-Tip for Organizers</h4>
224+
<p className="text-xs text-indigo-700/70 dark:text-indigo-300/60 mt-0.5">Use the handle on the left of each session to reorder items. Your schedule will automatically refresh for all attendees once saved.</p>
225+
</div>
226+
</div>
227+
</div>
228+
);
229+
};
230+
231+
export { ScheduleBuilder };
232+
export type { ScheduleBuilderProps };

0 commit comments

Comments
 (0)