@ludiks/react
Version:
Complete React library for Ludiks gamification platform - includes SDK and ready-to-use components
256 lines (255 loc) • 10.3 kB
JavaScript
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,
}
};
}