|
1 | 1 | // Notification.jsx |
2 | | -import React, { useState, useEffect } from 'react'; |
| 2 | +import React from 'react'; |
3 | 3 | import { motion, AnimatePresence } from 'framer-motion'; |
4 | | -import './Notification.css'; |
5 | 4 |
|
6 | | -const Notification = () => { |
7 | | - const [notifications, setNotifications] = useState([]); |
| 5 | +// 300px maxWidth + 20px container right offset + 50px extra |
| 6 | +const OFFSCREEN_X = 370; |
8 | 7 |
|
9 | | - useEffect(() => { |
10 | | - const handleNotification = (event) => { |
11 | | - const { message, duration, icon } = event.detail; |
12 | | - const id = Date.now(); |
13 | | - |
14 | | - setNotifications((prev) => { |
15 | | - if (prev.some((n) => n.message === message && Math.abs(n.id - id) < 1000)) { |
16 | | - return prev; |
17 | | - } |
18 | | - const iconSrc = typeof icon === 'string' ? icon : icon?.default; |
19 | | - return [...prev, { id, message, icon: iconSrc }]; |
20 | | - }); |
21 | | - |
22 | | - setTimeout(() => { |
23 | | - handleSwipe(id); |
24 | | - }, duration || 3000); |
25 | | - }; |
26 | | - |
27 | | - window.addEventListener('show-notification', handleNotification); |
28 | | - return () => { |
29 | | - window.removeEventListener('show-notification', handleNotification); |
30 | | - }; |
31 | | - }, []); |
| 8 | +const containerStyle = { |
| 9 | + position: 'absolute', |
| 10 | + top: 40, |
| 11 | + right: 20, |
| 12 | + display: 'flex', |
| 13 | + flexDirection: 'column', |
| 14 | + gap: '10px', |
| 15 | + zIndex: 300, |
| 16 | + pointerEvents: 'none', |
| 17 | +}; |
32 | 18 |
|
33 | | - const handleSwipe = (id) => { |
34 | | - setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, closing: true } : n))); |
| 19 | +const notificationStyle = { |
| 20 | + background: 'rgba(255, 255, 255, 0.7)', |
| 21 | + backdropFilter: 'blur(10px)', |
| 22 | + borderRadius: '12px', |
| 23 | + boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)', |
| 24 | + padding: '16px 20px', |
| 25 | + maxWidth: '300px', |
| 26 | + display: 'flex', |
| 27 | + alignItems: 'center', |
| 28 | + fontFamily: |
| 29 | + "-apple-system, BlinkMacSystemFont, 'San Francisco', 'Helvetica Neue', Helvetica, Arial, sans-serif", |
| 30 | + pointerEvents: 'auto', |
| 31 | + cursor: 'pointer', |
| 32 | + userSelect: 'none', |
| 33 | +}; |
35 | 34 |
|
36 | | - setTimeout(() => { |
37 | | - setNotifications((prev) => prev.filter((n) => n.id !== id)); |
38 | | - }, 500); |
39 | | - }; |
| 35 | +const iconStyle = { |
| 36 | + width: '40px', |
| 37 | + height: '40px', |
| 38 | + marginRight: '20px', |
| 39 | + borderRadius: '8px', |
| 40 | + objectFit: 'cover', |
| 41 | +}; |
40 | 42 |
|
41 | | - return ( |
42 | | - <div className="notification-container"> |
43 | | - <AnimatePresence> |
44 | | - {notifications.map(({ id, message, icon, closing }) => ( |
45 | | - <motion.div |
46 | | - key={id} |
47 | | - className="notification" |
48 | | - initial={{ x: 300, opacity: 0 }} |
49 | | - animate={closing ? { x: 300, opacity: 0 } : { x: 0, opacity: 1 }} |
50 | | - exit={{ x: 300, opacity: 0 }} |
51 | | - drag="x" |
52 | | - dragConstraints={{ left: 0, right: 300 }} |
53 | | - onDragEnd={(event, info) => { |
54 | | - if (info.offset.x > 150) { |
55 | | - handleSwipe(id); |
56 | | - } |
57 | | - }} |
58 | | - transition={{ duration: 0.5 }} |
59 | | - > |
60 | | - {icon && <img src={icon} alt="icon" className="notification-icon" />} |
61 | | - <span className="notification-message">{message}</span> |
62 | | - </motion.div> |
63 | | - ))} |
64 | | - </AnimatePresence> |
65 | | - </div> |
66 | | - ); |
| 43 | +const messageStyle = { |
| 44 | + fontSize: '14px', |
| 45 | + color: '#333', |
67 | 46 | }; |
68 | 47 |
|
69 | | -export default Notification; |
| 48 | +const Notification = ({ notifications, handleSwipe }) => ( |
| 49 | + <div style={containerStyle}> |
| 50 | + <AnimatePresence> |
| 51 | + {notifications.map(({ id, message, icon, closing }) => ( |
| 52 | + <motion.div |
| 53 | + key={id} |
| 54 | + style={notificationStyle} |
| 55 | + initial={{ x: OFFSCREEN_X, opacity: 0 }} |
| 56 | + animate={ |
| 57 | + closing |
| 58 | + ? { x: OFFSCREEN_X, opacity: 1 } // slide out fully |
| 59 | + : { x: 0, opacity: 1 } // slide in |
| 60 | + } |
| 61 | + exit={{ x: OFFSCREEN_X, opacity: 1 }} // keep full opacity |
| 62 | + drag="x" |
| 63 | + dragConstraints={{ left: 0, right: OFFSCREEN_X }} |
| 64 | + dragMomentum={false} |
| 65 | + onDragEnd={(e, info) => { |
| 66 | + // if dragged at all to the right, trigger full swipe |
| 67 | + if (info.offset.x > 0) { |
| 68 | + handleSwipe(id); |
| 69 | + } |
| 70 | + }} |
| 71 | + transition={{ duration: 0.5 }} |
| 72 | + > |
| 73 | + {icon && <img src={icon} alt="icon" style={iconStyle} />} |
| 74 | + <span style={messageStyle}>{message}</span> |
| 75 | + </motion.div> |
| 76 | + ))} |
| 77 | + </AnimatePresence> |
| 78 | + </div> |
| 79 | +); |
70 | 80 |
|
71 | | -export const notify = (message, duration = 3000, icon = '') => { |
72 | | - const iconSrc = typeof icon === 'string' ? icon : icon?.default; |
73 | | - const event = new CustomEvent('show-notification', { |
74 | | - detail: { message, duration, icon: iconSrc } |
75 | | - }); |
76 | | - window.dispatchEvent(event); |
77 | | -}; |
| 81 | +export default Notification; |
0 commit comments