-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathToastNotification.tsx
More file actions
129 lines (115 loc) · 3.87 KB
/
ToastNotification.tsx
File metadata and controls
129 lines (115 loc) · 3.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/**
* Phase 11: Toast Notification for Player Join/Leave
* Extended: URL fallback toast for clipboard failures
*
* Shows brief notifications when players join or leave the session.
* Also shows URL fallback when clipboard copy fails (iOS compatibility).
*/
import { useEffect, useState, useCallback, useRef } from 'react';
import { copyToClipboard } from '../utils/clipboard';
import './ToastNotification.css';
export interface Toast {
id: string;
message: string;
color?: string;
type: 'join' | 'leave' | 'url' | 'error' | 'warning';
/** For url type: the full URL to display */
url?: string;
}
interface ToastNotificationProps {
toasts: Toast[];
onDismiss: (id: string) => void;
}
export function ToastNotification({ toasts, onDismiss }: ToastNotificationProps) {
return (
<div className="toast-container">
{toasts.map((toast) => (
<ToastItem key={toast.id} toast={toast} onDismiss={onDismiss} />
))}
</div>
);
}
function ToastItem({ toast, onDismiss }: { toast: Toast; onDismiss: (id: string) => void }) {
const [isExiting, setIsExiting] = useState(false);
const [copyAttempted, setCopyAttempted] = useState(false);
const urlTapTimerRef = useRef<number | undefined>(undefined);
// Cleanup timer on unmount to prevent state update on unmounted component
useEffect(() => {
return () => {
if (urlTapTimerRef.current) {
clearTimeout(urlTapTimerRef.current);
}
};
}, []);
useEffect(() => {
// URL toasts stay longer (8s) so user can copy; others dismiss after 2.5s
const duration = toast.type === 'url' ? 8000 : 2500;
const timer = setTimeout(() => {
setIsExiting(true);
}, duration);
return () => clearTimeout(timer);
}, [toast.type]);
useEffect(() => {
if (isExiting) {
const timer = setTimeout(() => {
onDismiss(toast.id);
}, 300); // Match animation duration
return () => clearTimeout(timer);
}
}, [isExiting, toast.id, onDismiss]);
const handleUrlTap = useCallback(async () => {
if (toast.url) {
const success = await copyToClipboard(toast.url);
if (success) {
setCopyAttempted(true);
// Auto-dismiss after successful copy (timer cleaned up on unmount)
urlTapTimerRef.current = window.setTimeout(() => setIsExiting(true), 500);
}
}
}, [toast.url]);
const handleDismiss = useCallback(() => {
setIsExiting(true);
}, []);
// URL toast has special rendering
if (toast.type === 'url' && toast.url) {
return (
<div
className={`toast toast-url ${isExiting ? 'exiting' : ''}`}
onClick={handleUrlTap}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleUrlTap(); }}
role="button"
tabIndex={0}
>
<div className="toast-url-header">
<span className="toast-message">{toast.message}</span>
<button className="toast-dismiss" onClick={(e) => { e.stopPropagation(); handleDismiss(); }}>×</button>
</div>
<div className="toast-url-content">
<span className="toast-url-text">{toast.url}</span>
</div>
<div className="toast-url-hint">
{copyAttempted ? '✓ Copied!' : 'Tap to copy'}
</div>
</div>
);
}
// Standard join/leave/error/warning toast
const getIcon = () => {
switch (toast.type) {
case 'join': return '→';
case 'leave': return '←';
case 'error': return '⚠';
case 'warning': return '🔊';
default: return '•';
}
};
return (
<div
className={`toast ${toast.type} ${isExiting ? 'exiting' : ''}`}
style={{ '--toast-color': toast.type === 'error' ? '#e74c3c' : toast.type === 'warning' ? '#f39c12' : (toast.color ?? '#666') } as React.CSSProperties}
>
<span className="toast-icon">{getIcon()}</span>
<span className="toast-message">{toast.message}</span>
</div>
);
}