UNPKG

@ludiks/react

Version:

Complete React library for Ludiks gamification platform - includes SDK and ready-to-use components

256 lines (255 loc) 10.3 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useEffect, useState } from 'react'; import { commonStyles, keyframes } from '../../utils/styles'; export function GamificationToaster({ notification, position = 'top-right', duration = 4000, showRewards = true, showPoints = true, animations = true, onClose, visible = true, className, colors = {}, }) { const [isVisible, setIsVisible] = useState(visible); const [isAnimating, setIsAnimating] = useState(false); const [progress, setProgress] = useState(100); const styles = { success: colors.success || '#10b981', background: colors.background || '#ffffff', text: colors.text || '#1f2937', }; useEffect(() => { if (visible) { setIsVisible(true); if (animations) { setTimeout(() => setIsAnimating(true), 10); } else { setIsAnimating(true); } // Auto close after duration if (duration > 0) { const startTime = Date.now(); const interval = setInterval(() => { const elapsed = Date.now() - startTime; const remaining = Math.max(0, 100 - (elapsed / duration) * 100); setProgress(remaining); if (elapsed >= duration) { clearInterval(interval); handleClose(); } }, 16); // ~60fps return () => clearInterval(interval); } } }, [visible, duration, animations]); const handleClose = () => { if (animations) { setIsAnimating(false); setTimeout(() => { setIsVisible(false); onClose?.(); }, 300); } else { setIsVisible(false); onClose?.(); } }; const getIcon = () => { const iconStyle = { width: '40px', height: '40px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'white', fontSize: '20px', ...commonStyles.shadowLg, }; switch (notification.type) { case 'step-completed': return (_jsx("div", { style: { ...iconStyle, backgroundColor: styles.success }, children: _jsx("svg", { width: "24", height: "24", fill: "currentColor", viewBox: "0 0 20 20", children: _jsx("path", { fillRule: "evenodd", d: "M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z", clipRule: "evenodd" }) }) })); case 'circuit-completed': return (_jsx("div", { style: { ...iconStyle, backgroundColor: '#f59e0b' }, children: "\uD83C\uDFC6" })); case 'reward-earned': return (_jsx("div", { style: { ...iconStyle, backgroundColor: '#8b5cf6' }, children: "\uD83C\uDF81" })); default: return (_jsx("div", { style: { ...iconStyle, backgroundColor: styles.success }, children: "\u2B50" })); } }; const getPositionStyle = () => { const baseStyle = { position: 'fixed', zIndex: 50, maxWidth: '400px', width: '100%', }; switch (position) { case 'top-left': return { ...baseStyle, top: '16px', left: '16px' }; case 'top-center': return { ...baseStyle, top: '16px', left: '50%', transform: 'translateX(-50%)' }; case 'top-right': return { ...baseStyle, top: '16px', right: '16px' }; case 'bottom-left': return { ...baseStyle, bottom: '16px', left: '16px' }; case 'bottom-right': return { ...baseStyle, bottom: '16px', right: '16px' }; default: return { ...baseStyle, top: '16px', right: '16px' }; } }; const getAnimationStyle = () => { if (!animations) return {}; const baseStyle = { ...commonStyles.transitionSlow, }; if (isAnimating) { return { ...baseStyle, transform: 'translateX(0) translateY(0) scale(1)', opacity: 1, }; } else { const slideDirection = position.includes('right') ? 'translateX(100%)' : position.includes('left') ? 'translateX(-100%)' : position.includes('top') ? 'translateY(-100%)' : 'translateY(100%)'; return { ...baseStyle, transform: `${slideDirection} scale(0.95)`, opacity: 0, }; } }; if (!isVisible) return null; const containerStyle = { ...getPositionStyle(), ...getAnimationStyle(), }; const toasterStyle = { position: 'relative', backgroundColor: styles.background, borderRadius: '12px', ...commonStyles.shadowXl, border: '1px solid #f3f4f6', padding: '24px', backdropFilter: 'blur(8px)', }; const closeButtonStyle = { position: 'absolute', top: '12px', right: '12px', color: '#9ca3af', cursor: 'pointer', padding: '4px', borderRadius: '50%', border: 'none', background: 'transparent', ...commonStyles.transition, }; const contentStyle = { display: 'flex', alignItems: 'flex-start', gap: '16px', }; const textContentStyle = { flex: 1, minWidth: 0, paddingRight: '24px', }; const titleStyle = { fontSize: '16px', fontWeight: 'bold', color: styles.text, margin: 0, marginBottom: '8px', }; const pointsBadgeStyle = { display: 'inline-flex', alignItems: 'center', padding: '4px 12px', borderRadius: '20px', fontSize: '14px', fontWeight: '600', backgroundColor: `${styles.success}15`, color: styles.success, ...commonStyles.shadowSm, }; const descriptionStyle = { fontSize: '14px', color: '#6b7280', margin: '0 0 12px 0', lineHeight: '1.5', }; const rewardsSectionStyle = { marginTop: '8px', }; const rewardsTitleStyle = { fontSize: '12px', color: '#6b7280', fontWeight: '600', textTransform: 'uppercase', letterSpacing: '0.05em', margin: '0 0 8px 0', }; const rewardsContainerStyle = { display: 'flex', flexWrap: 'wrap', gap: '8px', }; const rewardBadgeStyle = { display: 'inline-flex', alignItems: 'center', padding: '6px 12px', borderRadius: '8px', fontSize: '14px', fontWeight: '500', backgroundColor: '#8b5cf615', color: '#8b5cf6', ...commonStyles.shadowSm, }; const progressBarStyle = { position: 'absolute', bottom: 0, left: 0, right: 0, height: '4px', backgroundColor: '#f3f4f6', borderRadius: '0 0 12px 12px', overflow: 'hidden', }; const progressFillStyle = { height: '100%', borderRadius: '0 0 12px 12px', backgroundColor: styles.success, width: `${progress}%`, ...commonStyles.transition, }; return (_jsxs("div", { style: containerStyle, className: className, children: [_jsxs("div", { style: toasterStyle, children: [_jsx("button", { onClick: handleClose, style: closeButtonStyle, onMouseEnter: (e) => { e.currentTarget.style.color = '#4b5563'; e.currentTarget.style.backgroundColor = '#f3f4f6'; }, onMouseLeave: (e) => { e.currentTarget.style.color = '#9ca3af'; e.currentTarget.style.backgroundColor = 'transparent'; }, children: _jsx("svg", { width: "20", height: "20", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) }), _jsxs("div", { style: contentStyle, children: [_jsx("div", { style: { flexShrink: 0 }, children: getIcon() }), _jsxs("div", { style: textContentStyle, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }, children: [_jsx("h4", { style: titleStyle, children: notification.title }), showPoints && notification.points && (_jsxs("span", { style: pointsBadgeStyle, children: ["+", notification.points, " pts"] }))] }), notification.description && (_jsx("p", { style: descriptionStyle, children: notification.description })), showRewards && notification.rewards && notification.rewards.length > 0 && (_jsxs("div", { style: rewardsSectionStyle, children: [_jsx("p", { style: rewardsTitleStyle, children: "Rewards unlocked:" }), _jsx("div", { style: rewardsContainerStyle, children: notification.rewards.map((reward, index) => (_jsxs("span", { style: rewardBadgeStyle, children: ["\uD83C\uDFC6 ", reward.name] }, index))) })] }))] })] }), duration > 0 && (_jsx("div", { style: progressBarStyle, children: _jsx("div", { style: progressFillStyle }) }))] }), _jsx("style", { dangerouslySetInnerHTML: { __html: keyframes } })] })); } // Utility function to create and show toaster export function showGamificationToast(notification, options = {}) { const container = document.createElement('div'); document.body.appendChild(container); const cleanup = () => { setTimeout(() => { if (container.parentNode) { container.parentNode.removeChild(container); } }, 350); }; // This would require ReactDOM.render in a real implementation // For now, we provide the component that can be used imperatively return { container, cleanup, toasterProps: { notification, onClose: cleanup, ...options, } }; }