react-achievements 3.5.0 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1127 -27
- package/dist/index.d.ts +197 -8
- package/dist/index.js +764 -32
- package/dist/index.js.map +1 -1
- package/dist/types/core/components/BadgesButton.d.ts +18 -3
- package/dist/types/core/hooks/useWindowSize.d.ts +16 -0
- package/dist/types/core/types.d.ts +2 -6
- package/dist/types/core/ui/BuiltInConfetti.d.ts +7 -0
- package/dist/types/core/ui/BuiltInModal.d.ts +7 -0
- package/dist/types/core/ui/BuiltInNotification.d.ts +7 -0
- package/dist/types/core/ui/LegacyWrappers.d.ts +21 -0
- package/dist/types/core/ui/interfaces.d.ts +131 -0
- package/dist/types/core/ui/legacyDetector.d.ts +40 -0
- package/dist/types/core/ui/themes.d.ts +14 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/providers/AchievementProvider.d.ts +14 -2
- package/package.json +15 -1
- package/dist/assets/defaultIcons.d.ts +0 -81
- package/dist/badges.d.ts +0 -8
- package/dist/components/Achievement.d.ts +0 -10
- package/dist/components/AchievementModal.d.ts +0 -12
- package/dist/components/Badge.d.ts +0 -9
- package/dist/components/BadgesButton.d.ts +0 -14
- package/dist/components/BadgesModal.d.ts +0 -12
- package/dist/components/ConfettiWrapper.d.ts +0 -6
- package/dist/components/Progress.d.ts +0 -6
- package/dist/context/AchievementContext.d.ts +0 -21
- package/dist/defaultStyles.d.ts +0 -19
- package/dist/hooks/useAchievement.d.ts +0 -8
- package/dist/hooks/useAchievementState.d.ts +0 -4
- package/dist/index.cjs.js +0 -2428
- package/dist/index.esm.js +0 -2403
- package/dist/levels.d.ts +0 -7
- package/dist/providers/AchievementProvider.d.ts +0 -12
- package/dist/redux/achievementSlice.d.ts +0 -30
- package/dist/redux/notificationSlice.d.ts +0 -7
- package/dist/redux/store.d.ts +0 -15
- package/dist/stories/Button.d.ts +0 -28
- package/dist/stories/Button.stories.d.ts +0 -23
- package/dist/stories/Header.d.ts +0 -13
- package/dist/stories/Header.stories.d.ts +0 -18
- package/dist/stories/Page.d.ts +0 -3
- package/dist/stories/Page.stories.d.ts +0 -12
- package/dist/types/core/context/AchievementContext.d.ts +0 -5
- package/dist/types/stories/Button.d.ts +0 -16
- package/dist/types/stories/Button.stories.d.ts +0 -23
- package/dist/types/stories/Header.d.ts +0 -13
- package/dist/types/stories/Header.stories.d.ts +0 -18
- package/dist/types/stories/Page.d.ts +0 -3
- package/dist/types/stories/Page.stories.d.ts +0 -12
- package/dist/types.d.ts +0 -37
- package/dist/utils/EventEmitter.d.ts +0 -6
package/dist/index.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useState, useEffect, createContext, useRef, useContext } from 'react';
|
|
2
2
|
import Modal from 'react-modal';
|
|
3
3
|
import Confetti from 'react-confetti';
|
|
4
|
-
import { useWindowSize } from 'react-use';
|
|
5
|
-
import { toast, ToastContainer } from 'react-toastify';
|
|
6
|
-
import 'react-toastify/dist/ReactToastify.css';
|
|
7
4
|
|
|
8
5
|
const isDate = (value) => {
|
|
9
6
|
return value instanceof Date;
|
|
@@ -861,6 +858,122 @@ class OfflineQueueStorage {
|
|
|
861
858
|
}
|
|
862
859
|
}
|
|
863
860
|
|
|
861
|
+
/**
|
|
862
|
+
* Built-in theme presets
|
|
863
|
+
*/
|
|
864
|
+
const builtInThemes = {
|
|
865
|
+
/**
|
|
866
|
+
* Modern theme - Dark gradients with vibrant accents
|
|
867
|
+
* Inspired by contemporary achievement systems (Discord, Steam, Xbox)
|
|
868
|
+
*/
|
|
869
|
+
modern: {
|
|
870
|
+
name: 'modern',
|
|
871
|
+
notification: {
|
|
872
|
+
background: 'linear-gradient(135deg, rgba(30, 30, 50, 0.98) 0%, rgba(50, 50, 70, 0.98) 100%)',
|
|
873
|
+
textColor: '#ffffff',
|
|
874
|
+
accentColor: '#4CAF50',
|
|
875
|
+
borderRadius: '12px',
|
|
876
|
+
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.4)',
|
|
877
|
+
fontSize: {
|
|
878
|
+
header: '12px',
|
|
879
|
+
title: '18px',
|
|
880
|
+
description: '14px',
|
|
881
|
+
},
|
|
882
|
+
},
|
|
883
|
+
modal: {
|
|
884
|
+
overlayColor: 'rgba(0, 0, 0, 0.85)',
|
|
885
|
+
background: 'linear-gradient(135deg, #1e1e32 0%, #323246 100%)',
|
|
886
|
+
textColor: '#ffffff',
|
|
887
|
+
accentColor: '#4CAF50',
|
|
888
|
+
borderRadius: '16px',
|
|
889
|
+
headerFontSize: '28px',
|
|
890
|
+
},
|
|
891
|
+
confetti: {
|
|
892
|
+
colors: ['#FFD700', '#4CAF50', '#2196F3', '#FF6B6B'],
|
|
893
|
+
particleCount: 50,
|
|
894
|
+
shapes: ['circle', 'square'],
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
/**
|
|
898
|
+
* Minimal theme - Clean, light design with subtle accents
|
|
899
|
+
* Perfect for professional or minimalist applications
|
|
900
|
+
*/
|
|
901
|
+
minimal: {
|
|
902
|
+
name: 'minimal',
|
|
903
|
+
notification: {
|
|
904
|
+
background: 'rgba(255, 255, 255, 0.98)',
|
|
905
|
+
textColor: '#333333',
|
|
906
|
+
accentColor: '#4CAF50',
|
|
907
|
+
borderRadius: '8px',
|
|
908
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
909
|
+
fontSize: {
|
|
910
|
+
header: '11px',
|
|
911
|
+
title: '16px',
|
|
912
|
+
description: '13px',
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
modal: {
|
|
916
|
+
overlayColor: 'rgba(0, 0, 0, 0.5)',
|
|
917
|
+
background: '#ffffff',
|
|
918
|
+
textColor: '#333333',
|
|
919
|
+
accentColor: '#4CAF50',
|
|
920
|
+
borderRadius: '12px',
|
|
921
|
+
headerFontSize: '24px',
|
|
922
|
+
},
|
|
923
|
+
confetti: {
|
|
924
|
+
colors: ['#4CAF50', '#2196F3'],
|
|
925
|
+
particleCount: 30,
|
|
926
|
+
shapes: ['circle'],
|
|
927
|
+
},
|
|
928
|
+
},
|
|
929
|
+
/**
|
|
930
|
+
* Gamified theme - Modern gaming aesthetic with sci-fi colors
|
|
931
|
+
* Dark navy backgrounds with cyan and orange accents (2024 gaming trend)
|
|
932
|
+
* Features square/badge-shaped achievement cards
|
|
933
|
+
*/
|
|
934
|
+
gamified: {
|
|
935
|
+
name: 'gamified',
|
|
936
|
+
notification: {
|
|
937
|
+
background: 'linear-gradient(135deg, rgba(5, 8, 22, 0.98) 0%, rgba(15, 23, 42, 0.98) 100%)',
|
|
938
|
+
textColor: '#22d3ee', // Bright cyan
|
|
939
|
+
accentColor: '#f97316', // Bright orange
|
|
940
|
+
borderRadius: '6px',
|
|
941
|
+
boxShadow: '0 8px 32px rgba(34, 211, 238, 0.4), 0 0 20px rgba(249, 115, 22, 0.3)',
|
|
942
|
+
fontSize: {
|
|
943
|
+
header: '13px',
|
|
944
|
+
title: '20px',
|
|
945
|
+
description: '15px',
|
|
946
|
+
},
|
|
947
|
+
},
|
|
948
|
+
modal: {
|
|
949
|
+
overlayColor: 'rgba(5, 8, 22, 0.85)',
|
|
950
|
+
background: 'linear-gradient(135deg, #0f172a 0%, #050816 100%)',
|
|
951
|
+
textColor: '#22d3ee', // Bright cyan
|
|
952
|
+
accentColor: '#f97316', // Bright orange
|
|
953
|
+
borderRadius: '8px',
|
|
954
|
+
headerFontSize: '32px',
|
|
955
|
+
achievementCardBorderRadius: '8px', // Square badge-like cards
|
|
956
|
+
achievementLayout: 'badge', // Use badge/grid layout instead of horizontal list
|
|
957
|
+
},
|
|
958
|
+
confetti: {
|
|
959
|
+
colors: ['#22d3ee', '#f97316', '#a855f7', '#eab308'], // Cyan, orange, purple, yellow
|
|
960
|
+
particleCount: 100,
|
|
961
|
+
shapes: ['circle', 'square'],
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
};
|
|
965
|
+
/**
|
|
966
|
+
* Retrieve a theme by name (internal use only)
|
|
967
|
+
* Only checks built-in themes
|
|
968
|
+
*
|
|
969
|
+
* @param name - Theme name (built-in only)
|
|
970
|
+
* @returns Theme configuration or undefined if not found
|
|
971
|
+
* @internal
|
|
972
|
+
*/
|
|
973
|
+
function getTheme(name) {
|
|
974
|
+
return builtInThemes[name];
|
|
975
|
+
}
|
|
976
|
+
|
|
864
977
|
const getPositionStyles = (position) => {
|
|
865
978
|
const base = {
|
|
866
979
|
position: 'fixed',
|
|
@@ -878,13 +991,34 @@ const getPositionStyles = (position) => {
|
|
|
878
991
|
return Object.assign(Object.assign({}, base), { bottom: 0, right: 0 });
|
|
879
992
|
}
|
|
880
993
|
};
|
|
881
|
-
const BadgesButton = ({ onClick, position, styles = {}, unlockedAchievements }) => {
|
|
882
|
-
|
|
994
|
+
const BadgesButton = ({ onClick, position = 'bottom-right', placement = 'fixed', styles = {}, unlockedAchievements, theme = 'modern', }) => {
|
|
995
|
+
// Get theme configuration for consistent styling
|
|
996
|
+
const themeConfig = getTheme(theme) || builtInThemes.modern;
|
|
997
|
+
const accentColor = themeConfig.notification.accentColor;
|
|
998
|
+
// Different styling for fixed vs inline placement
|
|
999
|
+
const baseStyles = placement === 'inline'
|
|
1000
|
+
? Object.assign({
|
|
1001
|
+
// Inline mode: looks like a navigation item
|
|
1002
|
+
backgroundColor: 'transparent', color: themeConfig.notification.textColor, padding: '12px 16px', border: 'none', borderRadius: '6px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', fontSize: '15px', width: '100%', textAlign: 'left', transition: 'background-color 0.2s ease-in-out' }, styles) : Object.assign(Object.assign({
|
|
1003
|
+
// Fixed mode: floating button
|
|
1004
|
+
backgroundColor: accentColor, color: 'white', padding: '10px 20px', border: 'none', borderRadius: '20px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px', fontSize: '16px', boxShadow: '0 2px 5px rgba(0,0,0,0.2)', transition: 'transform 0.2s ease-in-out' }, getPositionStyles(position)), styles);
|
|
883
1005
|
return (React.createElement("button", { onClick: onClick, style: baseStyles, onMouseEnter: (e) => {
|
|
884
|
-
|
|
1006
|
+
if (placement === 'inline') {
|
|
1007
|
+
// Inline mode: subtle background color change
|
|
1008
|
+
e.target.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
// Fixed mode: scale transformation
|
|
1012
|
+
e.target.style.transform = 'scale(1.05)';
|
|
1013
|
+
}
|
|
885
1014
|
}, onMouseLeave: (e) => {
|
|
886
|
-
|
|
887
|
-
|
|
1015
|
+
if (placement === 'inline') {
|
|
1016
|
+
e.target.style.backgroundColor = 'transparent';
|
|
1017
|
+
}
|
|
1018
|
+
else {
|
|
1019
|
+
e.target.style.transform = 'scale(1)';
|
|
1020
|
+
}
|
|
1021
|
+
}, "data-placement": placement, "data-testid": "badges-button" },
|
|
888
1022
|
"\uD83C\uDFC6 Achievements (",
|
|
889
1023
|
unlockedAchievements.length,
|
|
890
1024
|
")"));
|
|
@@ -1036,6 +1170,46 @@ const BadgesModal = ({ isOpen, onClose, achievements, styles = {}, icons = {}, s
|
|
|
1036
1170
|
})())));
|
|
1037
1171
|
};
|
|
1038
1172
|
|
|
1173
|
+
/**
|
|
1174
|
+
* Hook to track window dimensions
|
|
1175
|
+
* Replacement for react-use's useWindowSize
|
|
1176
|
+
*
|
|
1177
|
+
* @returns Object with width and height properties
|
|
1178
|
+
*
|
|
1179
|
+
* @example
|
|
1180
|
+
* ```tsx
|
|
1181
|
+
* const { width, height } = useWindowSize();
|
|
1182
|
+
* console.log(`Window size: ${width}x${height}`);
|
|
1183
|
+
* ```
|
|
1184
|
+
*/
|
|
1185
|
+
function useWindowSize() {
|
|
1186
|
+
const [size, setSize] = useState({
|
|
1187
|
+
width: typeof window !== 'undefined' ? window.innerWidth : 0,
|
|
1188
|
+
height: typeof window !== 'undefined' ? window.innerHeight : 0,
|
|
1189
|
+
});
|
|
1190
|
+
useEffect(() => {
|
|
1191
|
+
// Handle SSR - window may not be defined
|
|
1192
|
+
if (typeof window === 'undefined') {
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
const handleResize = () => {
|
|
1196
|
+
setSize({
|
|
1197
|
+
width: window.innerWidth,
|
|
1198
|
+
height: window.innerHeight,
|
|
1199
|
+
});
|
|
1200
|
+
};
|
|
1201
|
+
// Set initial size
|
|
1202
|
+
handleResize();
|
|
1203
|
+
// Add event listener
|
|
1204
|
+
window.addEventListener('resize', handleResize);
|
|
1205
|
+
// Cleanup
|
|
1206
|
+
return () => {
|
|
1207
|
+
window.removeEventListener('resize', handleResize);
|
|
1208
|
+
};
|
|
1209
|
+
}, []);
|
|
1210
|
+
return size;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1039
1213
|
const ConfettiWrapper = ({ show }) => {
|
|
1040
1214
|
const { width, height } = useWindowSize();
|
|
1041
1215
|
if (!show)
|
|
@@ -1350,8 +1524,547 @@ function preserveMetrics(current, imported) {
|
|
|
1350
1524
|
return preserved;
|
|
1351
1525
|
}
|
|
1352
1526
|
|
|
1527
|
+
/**
|
|
1528
|
+
* Built-in notification component
|
|
1529
|
+
* Modern, theme-aware achievement notification with smooth animations
|
|
1530
|
+
*/
|
|
1531
|
+
const BuiltInNotification = ({ achievement, onClose, duration = 5000, position = 'top-center', theme = 'modern', }) => {
|
|
1532
|
+
var _a, _b, _c;
|
|
1533
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
1534
|
+
const [isExiting, setIsExiting] = useState(false);
|
|
1535
|
+
// Get theme configuration
|
|
1536
|
+
const themeConfig = getTheme(theme) || builtInThemes.modern;
|
|
1537
|
+
const { notification: themeStyles } = themeConfig;
|
|
1538
|
+
useEffect(() => {
|
|
1539
|
+
// Slide in animation
|
|
1540
|
+
const showTimer = setTimeout(() => setIsVisible(true), 10);
|
|
1541
|
+
// Auto-dismiss
|
|
1542
|
+
const dismissTimer = setTimeout(() => {
|
|
1543
|
+
setIsExiting(true);
|
|
1544
|
+
setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
|
|
1545
|
+
}, duration);
|
|
1546
|
+
return () => {
|
|
1547
|
+
clearTimeout(showTimer);
|
|
1548
|
+
clearTimeout(dismissTimer);
|
|
1549
|
+
};
|
|
1550
|
+
}, [duration, onClose]);
|
|
1551
|
+
const getPositionStyles = () => {
|
|
1552
|
+
const base = {
|
|
1553
|
+
position: 'fixed',
|
|
1554
|
+
zIndex: 9999,
|
|
1555
|
+
};
|
|
1556
|
+
switch (position) {
|
|
1557
|
+
case 'top-center':
|
|
1558
|
+
return Object.assign(Object.assign({}, base), { top: 20, left: '50%', transform: 'translateX(-50%)' });
|
|
1559
|
+
case 'top-left':
|
|
1560
|
+
return Object.assign(Object.assign({}, base), { top: 20, left: 20 });
|
|
1561
|
+
case 'top-right':
|
|
1562
|
+
return Object.assign(Object.assign({}, base), { top: 20, right: 20 });
|
|
1563
|
+
case 'bottom-center':
|
|
1564
|
+
return Object.assign(Object.assign({}, base), { bottom: 20, left: '50%', transform: 'translateX(-50%)' });
|
|
1565
|
+
case 'bottom-left':
|
|
1566
|
+
return Object.assign(Object.assign({}, base), { bottom: 20, left: 20 });
|
|
1567
|
+
case 'bottom-right':
|
|
1568
|
+
return Object.assign(Object.assign({}, base), { bottom: 20, right: 20 });
|
|
1569
|
+
default:
|
|
1570
|
+
return Object.assign(Object.assign({}, base), { top: 20, left: '50%', transform: 'translateX(-50%)' });
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
const containerStyles = Object.assign(Object.assign({}, getPositionStyles()), { background: themeStyles.background, borderRadius: themeStyles.borderRadius, boxShadow: themeStyles.boxShadow, padding: '16px 24px', minWidth: '320px', maxWidth: '500px', display: 'flex', alignItems: 'center', gap: '16px', opacity: isVisible && !isExiting ? 1 : 0, transform: position.startsWith('top')
|
|
1574
|
+
? `translateY(${isVisible && !isExiting ? '0' : '-20px'}) ${position.includes('center') ? 'translateX(-50%)' : ''}`
|
|
1575
|
+
: `translateY(${isVisible && !isExiting ? '0' : '20px'}) ${position.includes('center') ? 'translateX(-50%)' : ''}`, transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', pointerEvents: isVisible ? 'auto' : 'none' });
|
|
1576
|
+
const iconStyles = {
|
|
1577
|
+
fontSize: '48px',
|
|
1578
|
+
lineHeight: 1,
|
|
1579
|
+
flexShrink: 0,
|
|
1580
|
+
};
|
|
1581
|
+
const contentStyles = {
|
|
1582
|
+
flex: 1,
|
|
1583
|
+
color: themeStyles.textColor,
|
|
1584
|
+
minWidth: 0,
|
|
1585
|
+
};
|
|
1586
|
+
const headerStyles = {
|
|
1587
|
+
fontSize: ((_a = themeStyles.fontSize) === null || _a === void 0 ? void 0 : _a.header) || '12px',
|
|
1588
|
+
textTransform: 'uppercase',
|
|
1589
|
+
letterSpacing: '1px',
|
|
1590
|
+
opacity: 0.8,
|
|
1591
|
+
marginBottom: '4px',
|
|
1592
|
+
color: themeStyles.accentColor,
|
|
1593
|
+
fontWeight: 600,
|
|
1594
|
+
};
|
|
1595
|
+
const titleStyles = {
|
|
1596
|
+
fontSize: ((_b = themeStyles.fontSize) === null || _b === void 0 ? void 0 : _b.title) || '18px',
|
|
1597
|
+
fontWeight: 'bold',
|
|
1598
|
+
marginBottom: '4px',
|
|
1599
|
+
overflow: 'hidden',
|
|
1600
|
+
textOverflow: 'ellipsis',
|
|
1601
|
+
whiteSpace: 'nowrap',
|
|
1602
|
+
};
|
|
1603
|
+
const descriptionStyles = {
|
|
1604
|
+
fontSize: ((_c = themeStyles.fontSize) === null || _c === void 0 ? void 0 : _c.description) || '14px',
|
|
1605
|
+
opacity: 0.9,
|
|
1606
|
+
overflow: 'hidden',
|
|
1607
|
+
textOverflow: 'ellipsis',
|
|
1608
|
+
display: '-webkit-box',
|
|
1609
|
+
WebkitLineClamp: 2,
|
|
1610
|
+
WebkitBoxOrient: 'vertical',
|
|
1611
|
+
};
|
|
1612
|
+
const closeButtonStyles = {
|
|
1613
|
+
background: 'none',
|
|
1614
|
+
border: 'none',
|
|
1615
|
+
color: themeStyles.textColor,
|
|
1616
|
+
fontSize: '24px',
|
|
1617
|
+
cursor: 'pointer',
|
|
1618
|
+
opacity: 0.6,
|
|
1619
|
+
transition: 'opacity 0.2s',
|
|
1620
|
+
padding: '4px',
|
|
1621
|
+
flexShrink: 0,
|
|
1622
|
+
lineHeight: 1,
|
|
1623
|
+
};
|
|
1624
|
+
return (React.createElement("div", { style: containerStyles, "data-testid": "built-in-notification" },
|
|
1625
|
+
React.createElement("div", { style: iconStyles }, achievement.icon),
|
|
1626
|
+
React.createElement("div", { style: contentStyles },
|
|
1627
|
+
React.createElement("div", { style: headerStyles }, "Achievement Unlocked!"),
|
|
1628
|
+
React.createElement("div", { style: titleStyles }, achievement.title),
|
|
1629
|
+
achievement.description && (React.createElement("div", { style: descriptionStyles }, achievement.description))),
|
|
1630
|
+
React.createElement("button", { onClick: () => {
|
|
1631
|
+
setIsExiting(true);
|
|
1632
|
+
setTimeout(() => onClose === null || onClose === void 0 ? void 0 : onClose(), 300);
|
|
1633
|
+
}, style: closeButtonStyles, onMouseEnter: (e) => (e.currentTarget.style.opacity = '1'), onMouseLeave: (e) => (e.currentTarget.style.opacity = '0.6'), "aria-label": "Close notification" }, "\u00D7")));
|
|
1634
|
+
};
|
|
1635
|
+
|
|
1636
|
+
/**
|
|
1637
|
+
* Built-in confetti component
|
|
1638
|
+
* Lightweight CSS-based confetti animation
|
|
1639
|
+
*/
|
|
1640
|
+
const BuiltInConfetti = ({ show, duration = 5000, particleCount = 50, colors = ['#FFD700', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7'], }) => {
|
|
1641
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
1642
|
+
const { width, height } = useWindowSize();
|
|
1643
|
+
useEffect(() => {
|
|
1644
|
+
if (show) {
|
|
1645
|
+
setIsVisible(true);
|
|
1646
|
+
const timer = setTimeout(() => setIsVisible(false), duration);
|
|
1647
|
+
return () => clearTimeout(timer);
|
|
1648
|
+
}
|
|
1649
|
+
else {
|
|
1650
|
+
setIsVisible(false);
|
|
1651
|
+
}
|
|
1652
|
+
}, [show, duration]);
|
|
1653
|
+
if (!isVisible)
|
|
1654
|
+
return null;
|
|
1655
|
+
const containerStyles = {
|
|
1656
|
+
position: 'fixed',
|
|
1657
|
+
top: 0,
|
|
1658
|
+
left: 0,
|
|
1659
|
+
width: '100%',
|
|
1660
|
+
height: '100%',
|
|
1661
|
+
pointerEvents: 'none',
|
|
1662
|
+
zIndex: 10001,
|
|
1663
|
+
overflow: 'hidden',
|
|
1664
|
+
};
|
|
1665
|
+
// Generate particles
|
|
1666
|
+
const particles = Array.from({ length: particleCount }, (_, i) => {
|
|
1667
|
+
const color = colors[Math.floor(Math.random() * colors.length)];
|
|
1668
|
+
const startX = Math.random() * width;
|
|
1669
|
+
const rotation = Math.random() * 360;
|
|
1670
|
+
const fallDuration = 3 + Math.random() * 2; // 3-5 seconds
|
|
1671
|
+
const delay = Math.random() * 0.5; // 0-0.5s delay
|
|
1672
|
+
const shape = Math.random() > 0.5 ? 'circle' : 'square';
|
|
1673
|
+
const particleStyles = {
|
|
1674
|
+
position: 'absolute',
|
|
1675
|
+
top: '-20px',
|
|
1676
|
+
left: `${startX}px`,
|
|
1677
|
+
width: '10px',
|
|
1678
|
+
height: '10px',
|
|
1679
|
+
backgroundColor: color,
|
|
1680
|
+
borderRadius: shape === 'circle' ? '50%' : '0',
|
|
1681
|
+
transform: `rotate(${rotation}deg)`,
|
|
1682
|
+
animation: `confettiFall ${fallDuration}s linear ${delay}s forwards`,
|
|
1683
|
+
opacity: 0.9,
|
|
1684
|
+
};
|
|
1685
|
+
return React.createElement("div", { key: i, style: particleStyles, "data-testid": "confetti-particle" });
|
|
1686
|
+
});
|
|
1687
|
+
return (React.createElement(React.Fragment, null,
|
|
1688
|
+
React.createElement("style", null, `
|
|
1689
|
+
@keyframes confettiFall {
|
|
1690
|
+
0% {
|
|
1691
|
+
transform: translateY(0) rotate(0deg);
|
|
1692
|
+
opacity: 1;
|
|
1693
|
+
}
|
|
1694
|
+
100% {
|
|
1695
|
+
transform: translateY(${height + 50}px) rotate(720deg);
|
|
1696
|
+
opacity: 0;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
`),
|
|
1700
|
+
React.createElement("div", { style: containerStyles, "data-testid": "built-in-confetti" }, particles)));
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
/**
|
|
1704
|
+
* Legacy UI library detection system
|
|
1705
|
+
* Attempts to dynamically import external UI libraries
|
|
1706
|
+
* Shows deprecation warnings when detected
|
|
1707
|
+
*/
|
|
1708
|
+
let cachedLibraries = null;
|
|
1709
|
+
let detectionAttempted = false;
|
|
1710
|
+
let deprecationWarningShown = false;
|
|
1711
|
+
/**
|
|
1712
|
+
* Attempts to dynamically import legacy UI libraries
|
|
1713
|
+
* Uses try/catch to gracefully handle missing dependencies
|
|
1714
|
+
* Caches result to avoid multiple import attempts
|
|
1715
|
+
*
|
|
1716
|
+
* @returns Promise resolving to LegacyLibraries object
|
|
1717
|
+
*/
|
|
1718
|
+
function detectLegacyLibraries() {
|
|
1719
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1720
|
+
if (detectionAttempted && cachedLibraries !== null) {
|
|
1721
|
+
return cachedLibraries;
|
|
1722
|
+
}
|
|
1723
|
+
detectionAttempted = true;
|
|
1724
|
+
const libraries = {};
|
|
1725
|
+
// Try to import react-toastify
|
|
1726
|
+
try {
|
|
1727
|
+
const toastifyModule = yield import('react-toastify');
|
|
1728
|
+
libraries.toast = toastifyModule.toast;
|
|
1729
|
+
libraries.ToastContainer = toastifyModule.ToastContainer;
|
|
1730
|
+
}
|
|
1731
|
+
catch (_a) {
|
|
1732
|
+
// Not installed, will use built-in notification
|
|
1733
|
+
}
|
|
1734
|
+
// Try to import react-modal
|
|
1735
|
+
try {
|
|
1736
|
+
const modalModule = yield import('react-modal');
|
|
1737
|
+
libraries.Modal = modalModule.default;
|
|
1738
|
+
}
|
|
1739
|
+
catch (_b) {
|
|
1740
|
+
// Not installed, will use built-in modal
|
|
1741
|
+
}
|
|
1742
|
+
// Try to import react-confetti
|
|
1743
|
+
try {
|
|
1744
|
+
const confettiModule = yield import('react-confetti');
|
|
1745
|
+
libraries.Confetti = confettiModule.default;
|
|
1746
|
+
}
|
|
1747
|
+
catch (_c) {
|
|
1748
|
+
// Not installed, will use built-in confetti
|
|
1749
|
+
}
|
|
1750
|
+
// Try to import react-use (only for useWindowSize)
|
|
1751
|
+
try {
|
|
1752
|
+
const reactUseModule = yield import('react-use');
|
|
1753
|
+
libraries.useWindowSize = reactUseModule.useWindowSize;
|
|
1754
|
+
}
|
|
1755
|
+
catch (_d) {
|
|
1756
|
+
// Not installed, will use built-in useWindowSize
|
|
1757
|
+
}
|
|
1758
|
+
cachedLibraries = libraries;
|
|
1759
|
+
// Show deprecation warning if ANY legacy library is found
|
|
1760
|
+
if (!deprecationWarningShown && Object.keys(libraries).length > 0) {
|
|
1761
|
+
const foundLibraries = [];
|
|
1762
|
+
if (libraries.toast)
|
|
1763
|
+
foundLibraries.push('react-toastify');
|
|
1764
|
+
if (libraries.Modal)
|
|
1765
|
+
foundLibraries.push('react-modal');
|
|
1766
|
+
if (libraries.Confetti)
|
|
1767
|
+
foundLibraries.push('react-confetti');
|
|
1768
|
+
if (libraries.useWindowSize)
|
|
1769
|
+
foundLibraries.push('react-use');
|
|
1770
|
+
console.warn(`[react-achievements] DEPRECATION WARNING: External UI dependencies (${foundLibraries.join(', ')}) are deprecated and will become fully optional in v4.0.0.\n\n` +
|
|
1771
|
+
`The library now includes built-in UI components with modern design and theme support.\n\n` +
|
|
1772
|
+
`To migrate:\n` +
|
|
1773
|
+
`1. Add "useBuiltInUI={true}" to your AchievementProvider\n` +
|
|
1774
|
+
`2. Test your application (UI will change to modern theme)\n` +
|
|
1775
|
+
`3. Optionally customize with theme="minimal" or theme="gamified"\n` +
|
|
1776
|
+
`4. Remove external dependencies from package.json\n\n` +
|
|
1777
|
+
`To silence this warning, set useBuiltInUI={true} in AchievementProvider.\n\n` +
|
|
1778
|
+
`Learn more: https://github.com/dave-b-b/react-achievements#migration-guide`);
|
|
1779
|
+
deprecationWarningShown = true;
|
|
1780
|
+
}
|
|
1781
|
+
return libraries;
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
/**
|
|
1786
|
+
* Built-in modal component
|
|
1787
|
+
* Modern, theme-aware achievement modal with smooth animations
|
|
1788
|
+
*/
|
|
1789
|
+
const BuiltInModal = ({ isOpen, onClose, achievements, icons = {}, theme = 'modern', }) => {
|
|
1790
|
+
// Merge custom icons with defaults
|
|
1791
|
+
const mergedIcons = Object.assign(Object.assign({}, defaultAchievementIcons), icons);
|
|
1792
|
+
// Get theme configuration
|
|
1793
|
+
const themeConfig = getTheme(theme) || builtInThemes.modern;
|
|
1794
|
+
const { modal: themeStyles } = themeConfig;
|
|
1795
|
+
useEffect(() => {
|
|
1796
|
+
if (isOpen) {
|
|
1797
|
+
// Lock body scroll when modal is open
|
|
1798
|
+
document.body.style.overflow = 'hidden';
|
|
1799
|
+
}
|
|
1800
|
+
else {
|
|
1801
|
+
// Restore body scroll
|
|
1802
|
+
document.body.style.overflow = '';
|
|
1803
|
+
}
|
|
1804
|
+
return () => {
|
|
1805
|
+
document.body.style.overflow = '';
|
|
1806
|
+
};
|
|
1807
|
+
}, [isOpen]);
|
|
1808
|
+
if (!isOpen)
|
|
1809
|
+
return null;
|
|
1810
|
+
const overlayStyles = {
|
|
1811
|
+
position: 'fixed',
|
|
1812
|
+
top: 0,
|
|
1813
|
+
left: 0,
|
|
1814
|
+
right: 0,
|
|
1815
|
+
bottom: 0,
|
|
1816
|
+
backgroundColor: themeStyles.overlayColor,
|
|
1817
|
+
display: 'flex',
|
|
1818
|
+
alignItems: 'center',
|
|
1819
|
+
justifyContent: 'center',
|
|
1820
|
+
zIndex: 10000,
|
|
1821
|
+
animation: 'fadeIn 0.3s ease-in-out',
|
|
1822
|
+
};
|
|
1823
|
+
const modalStyles = {
|
|
1824
|
+
background: themeStyles.background,
|
|
1825
|
+
borderRadius: themeStyles.borderRadius,
|
|
1826
|
+
padding: '32px',
|
|
1827
|
+
maxWidth: '600px',
|
|
1828
|
+
width: '90%',
|
|
1829
|
+
maxHeight: '80vh',
|
|
1830
|
+
overflow: 'auto',
|
|
1831
|
+
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.5)',
|
|
1832
|
+
animation: 'scaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
|
1833
|
+
position: 'relative',
|
|
1834
|
+
};
|
|
1835
|
+
const headerStyles = {
|
|
1836
|
+
display: 'flex',
|
|
1837
|
+
justifyContent: 'space-between',
|
|
1838
|
+
alignItems: 'center',
|
|
1839
|
+
marginBottom: '24px',
|
|
1840
|
+
};
|
|
1841
|
+
const titleStyles = {
|
|
1842
|
+
margin: 0,
|
|
1843
|
+
color: themeStyles.textColor,
|
|
1844
|
+
fontSize: themeStyles.headerFontSize || '28px',
|
|
1845
|
+
fontWeight: 'bold',
|
|
1846
|
+
};
|
|
1847
|
+
const closeButtonStyles = {
|
|
1848
|
+
background: 'none',
|
|
1849
|
+
border: 'none',
|
|
1850
|
+
fontSize: '32px',
|
|
1851
|
+
cursor: 'pointer',
|
|
1852
|
+
color: themeStyles.textColor,
|
|
1853
|
+
opacity: 0.6,
|
|
1854
|
+
transition: 'opacity 0.2s',
|
|
1855
|
+
padding: 0,
|
|
1856
|
+
lineHeight: 1,
|
|
1857
|
+
};
|
|
1858
|
+
const isBadgeLayout = themeStyles.achievementLayout === 'badge';
|
|
1859
|
+
const listStyles = isBadgeLayout
|
|
1860
|
+
? {
|
|
1861
|
+
display: 'grid',
|
|
1862
|
+
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
|
|
1863
|
+
gap: '16px',
|
|
1864
|
+
}
|
|
1865
|
+
: {
|
|
1866
|
+
display: 'flex',
|
|
1867
|
+
flexDirection: 'column',
|
|
1868
|
+
gap: '12px',
|
|
1869
|
+
};
|
|
1870
|
+
const getAchievementItemStyles = (isUnlocked) => {
|
|
1871
|
+
const baseStyles = {
|
|
1872
|
+
borderRadius: themeStyles.achievementCardBorderRadius || '12px',
|
|
1873
|
+
backgroundColor: isUnlocked
|
|
1874
|
+
? `${themeStyles.accentColor}1A` // 10% opacity
|
|
1875
|
+
: 'rgba(255, 255, 255, 0.05)',
|
|
1876
|
+
border: `2px solid ${isUnlocked ? themeStyles.accentColor : 'rgba(255, 255, 255, 0.1)'}`,
|
|
1877
|
+
opacity: isUnlocked ? 1 : 0.5,
|
|
1878
|
+
transition: 'all 0.2s',
|
|
1879
|
+
};
|
|
1880
|
+
if (isBadgeLayout) {
|
|
1881
|
+
// Badge layout: vertical, centered, square-ish
|
|
1882
|
+
return Object.assign(Object.assign({}, baseStyles), { display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center', padding: '20px 12px', aspectRatio: '1 / 1.1', minHeight: '160px' });
|
|
1883
|
+
}
|
|
1884
|
+
else {
|
|
1885
|
+
// Horizontal layout (default)
|
|
1886
|
+
return Object.assign(Object.assign({}, baseStyles), { display: 'flex', gap: '16px', padding: '16px' });
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
const getIconContainerStyles = (isUnlocked) => {
|
|
1890
|
+
if (isBadgeLayout) {
|
|
1891
|
+
return {
|
|
1892
|
+
fontSize: '48px',
|
|
1893
|
+
lineHeight: 1,
|
|
1894
|
+
marginBottom: '8px',
|
|
1895
|
+
opacity: isUnlocked ? 1 : 0.3,
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
return {
|
|
1899
|
+
fontSize: '40px',
|
|
1900
|
+
flexShrink: 0,
|
|
1901
|
+
lineHeight: 1,
|
|
1902
|
+
opacity: isUnlocked ? 1 : 0.3,
|
|
1903
|
+
};
|
|
1904
|
+
};
|
|
1905
|
+
const contentStyles = isBadgeLayout
|
|
1906
|
+
? {
|
|
1907
|
+
width: '100%',
|
|
1908
|
+
}
|
|
1909
|
+
: {
|
|
1910
|
+
flex: 1,
|
|
1911
|
+
minWidth: 0,
|
|
1912
|
+
};
|
|
1913
|
+
const achievementTitleStyles = isBadgeLayout
|
|
1914
|
+
? {
|
|
1915
|
+
margin: '0 0 4px 0',
|
|
1916
|
+
color: themeStyles.textColor,
|
|
1917
|
+
fontSize: '14px',
|
|
1918
|
+
fontWeight: 'bold',
|
|
1919
|
+
lineHeight: '1.3',
|
|
1920
|
+
}
|
|
1921
|
+
: {
|
|
1922
|
+
margin: '0 0 8px 0',
|
|
1923
|
+
color: themeStyles.textColor,
|
|
1924
|
+
fontSize: '18px',
|
|
1925
|
+
fontWeight: 'bold',
|
|
1926
|
+
overflow: 'hidden',
|
|
1927
|
+
textOverflow: 'ellipsis',
|
|
1928
|
+
whiteSpace: 'nowrap',
|
|
1929
|
+
};
|
|
1930
|
+
const achievementDescriptionStyles = isBadgeLayout
|
|
1931
|
+
? {
|
|
1932
|
+
margin: 0,
|
|
1933
|
+
color: themeStyles.textColor,
|
|
1934
|
+
opacity: 0.7,
|
|
1935
|
+
fontSize: '11px',
|
|
1936
|
+
lineHeight: '1.3',
|
|
1937
|
+
}
|
|
1938
|
+
: {
|
|
1939
|
+
margin: 0,
|
|
1940
|
+
color: themeStyles.textColor,
|
|
1941
|
+
opacity: 0.8,
|
|
1942
|
+
fontSize: '14px',
|
|
1943
|
+
};
|
|
1944
|
+
const getLockIconStyles = () => {
|
|
1945
|
+
if (isBadgeLayout) {
|
|
1946
|
+
return {
|
|
1947
|
+
position: 'absolute',
|
|
1948
|
+
top: '8px',
|
|
1949
|
+
right: '8px',
|
|
1950
|
+
fontSize: '18px',
|
|
1951
|
+
opacity: 0.6,
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
return {
|
|
1955
|
+
fontSize: '24px',
|
|
1956
|
+
flexShrink: 0,
|
|
1957
|
+
opacity: 0.5,
|
|
1958
|
+
};
|
|
1959
|
+
};
|
|
1960
|
+
return (React.createElement(React.Fragment, null,
|
|
1961
|
+
React.createElement("style", null, `
|
|
1962
|
+
@keyframes fadeIn {
|
|
1963
|
+
from { opacity: 0; }
|
|
1964
|
+
to { opacity: 1; }
|
|
1965
|
+
}
|
|
1966
|
+
@keyframes scaleIn {
|
|
1967
|
+
from {
|
|
1968
|
+
transform: scale(0.9);
|
|
1969
|
+
opacity: 0;
|
|
1970
|
+
}
|
|
1971
|
+
to {
|
|
1972
|
+
transform: scale(1);
|
|
1973
|
+
opacity: 1;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
`),
|
|
1977
|
+
React.createElement("div", { style: overlayStyles, onClick: onClose, "data-testid": "built-in-modal-overlay" },
|
|
1978
|
+
React.createElement("div", { style: modalStyles, onClick: (e) => e.stopPropagation(), "data-testid": "built-in-modal" },
|
|
1979
|
+
React.createElement("div", { style: headerStyles },
|
|
1980
|
+
React.createElement("h2", { style: titleStyles }, "\uD83C\uDFC6 Achievements"),
|
|
1981
|
+
React.createElement("button", { onClick: onClose, style: closeButtonStyles, onMouseEnter: (e) => (e.currentTarget.style.opacity = '1'), onMouseLeave: (e) => (e.currentTarget.style.opacity = '0.6'), "aria-label": "Close modal" }, "\u00D7")),
|
|
1982
|
+
React.createElement("div", { style: listStyles }, achievements.length === 0 ? (React.createElement("div", { style: { textAlign: 'center', padding: '40px 20px', color: themeStyles.textColor, opacity: 0.6 } }, "No achievements yet. Start exploring to unlock them!")) : (achievements.map((achievement) => {
|
|
1983
|
+
const icon = (achievement.achievementIconKey &&
|
|
1984
|
+
mergedIcons[achievement.achievementIconKey]) ||
|
|
1985
|
+
mergedIcons.default ||
|
|
1986
|
+
'⭐';
|
|
1987
|
+
return (React.createElement("div", { key: achievement.achievementId, style: Object.assign(Object.assign({}, getAchievementItemStyles(achievement.isUnlocked)), { position: isBadgeLayout ? 'relative' : 'static' }) },
|
|
1988
|
+
React.createElement("div", { style: getIconContainerStyles(achievement.isUnlocked) }, icon),
|
|
1989
|
+
React.createElement("div", { style: contentStyles },
|
|
1990
|
+
React.createElement("h3", { style: achievementTitleStyles }, achievement.achievementTitle),
|
|
1991
|
+
React.createElement("p", { style: achievementDescriptionStyles }, achievement.achievementDescription)),
|
|
1992
|
+
!achievement.isUnlocked && (React.createElement("div", { style: getLockIconStyles() }, "\uD83D\uDD12"))));
|
|
1993
|
+
})))))));
|
|
1994
|
+
};
|
|
1995
|
+
|
|
1996
|
+
/**
|
|
1997
|
+
* Legacy library wrappers for backwards compatibility
|
|
1998
|
+
* Wraps external UI libraries to match our component interfaces
|
|
1999
|
+
*/
|
|
2000
|
+
/**
|
|
2001
|
+
* Wrapper for react-toastify toast notifications
|
|
2002
|
+
* Falls back to built-in notification if not available
|
|
2003
|
+
*/
|
|
2004
|
+
const createLegacyToastNotification = (libraries) => {
|
|
2005
|
+
return ({ achievement, onClose }) => {
|
|
2006
|
+
const { toast } = libraries;
|
|
2007
|
+
useEffect(() => {
|
|
2008
|
+
if (!toast)
|
|
2009
|
+
return;
|
|
2010
|
+
// Call toast.success with achievement content
|
|
2011
|
+
toast.success(React.createElement("div", { style: { display: 'flex', alignItems: 'center' } },
|
|
2012
|
+
React.createElement("span", { style: { fontSize: '2em', marginRight: '10px' } }, achievement.icon),
|
|
2013
|
+
React.createElement("div", null,
|
|
2014
|
+
React.createElement("div", { style: { fontSize: '12px', opacity: 0.8, marginBottom: '4px' } }, "Achievement Unlocked!"),
|
|
2015
|
+
React.createElement("div", { style: { fontWeight: 'bold', marginBottom: '4px' } }, achievement.title),
|
|
2016
|
+
achievement.description && (React.createElement("div", { style: { fontSize: '13px', opacity: 0.9 } }, achievement.description)))), {
|
|
2017
|
+
position: 'top-right',
|
|
2018
|
+
autoClose: 5000,
|
|
2019
|
+
hideProgressBar: false,
|
|
2020
|
+
closeOnClick: true,
|
|
2021
|
+
pauseOnHover: true,
|
|
2022
|
+
draggable: true,
|
|
2023
|
+
toastId: achievement.id,
|
|
2024
|
+
onClose,
|
|
2025
|
+
});
|
|
2026
|
+
}, [achievement, toast, onClose]);
|
|
2027
|
+
return null; // Toast handles its own rendering
|
|
2028
|
+
};
|
|
2029
|
+
};
|
|
2030
|
+
/**
|
|
2031
|
+
* Wrapper for react-confetti Confetti component
|
|
2032
|
+
* Falls back to built-in confetti if not available
|
|
2033
|
+
*/
|
|
2034
|
+
const createLegacyConfettiWrapper = (libraries) => {
|
|
2035
|
+
return ({ show, duration = 5000, particleCount = 200, colors }) => {
|
|
2036
|
+
const { Confetti, useWindowSize: legacyUseWindowSize } = libraries;
|
|
2037
|
+
// If Confetti not available, use built-in
|
|
2038
|
+
if (!Confetti) {
|
|
2039
|
+
return (React.createElement(BuiltInConfetti, { show: show, duration: duration, particleCount: particleCount, colors: colors }));
|
|
2040
|
+
}
|
|
2041
|
+
// Use react-confetti with react-use's useWindowSize if available
|
|
2042
|
+
// Otherwise fall back to default dimensions
|
|
2043
|
+
let width = 0;
|
|
2044
|
+
let height = 0;
|
|
2045
|
+
if (legacyUseWindowSize) {
|
|
2046
|
+
const size = legacyUseWindowSize();
|
|
2047
|
+
width = size.width;
|
|
2048
|
+
height = size.height;
|
|
2049
|
+
}
|
|
2050
|
+
else if (typeof window !== 'undefined') {
|
|
2051
|
+
width = window.innerWidth;
|
|
2052
|
+
height = window.innerHeight;
|
|
2053
|
+
}
|
|
2054
|
+
if (!show)
|
|
2055
|
+
return null;
|
|
2056
|
+
return (React.createElement(Confetti, { width: width, height: height, numberOfPieces: particleCount, recycle: false, colors: colors, style: {
|
|
2057
|
+
position: 'fixed',
|
|
2058
|
+
top: 0,
|
|
2059
|
+
left: 0,
|
|
2060
|
+
zIndex: 10001,
|
|
2061
|
+
pointerEvents: 'none',
|
|
2062
|
+
} }));
|
|
2063
|
+
};
|
|
2064
|
+
};
|
|
2065
|
+
|
|
1353
2066
|
const AchievementContext = createContext(undefined);
|
|
1354
|
-
const AchievementProvider = ({ achievements: achievementsConfig, storage = StorageType.Local, children, icons = {}, onError, restApiConfig, }) => {
|
|
2067
|
+
const AchievementProvider = ({ achievements: achievementsConfig, storage = StorageType.Local, children, icons = {}, onError, restApiConfig, ui = {}, useBuiltInUI = false, }) => {
|
|
1355
2068
|
// Normalize the configuration to the complex format
|
|
1356
2069
|
const achievements = normalizeAchievements(achievementsConfig);
|
|
1357
2070
|
const [achievementState, setAchievementState] = useState({
|
|
@@ -1365,6 +2078,10 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
|
|
|
1365
2078
|
const metricsUpdatedRef = useRef(false);
|
|
1366
2079
|
const [showConfetti, setShowConfetti] = useState(false);
|
|
1367
2080
|
const [_currentAchievement, setCurrentAchievement] = useState(null);
|
|
2081
|
+
// NEW: UI component resolution state (v3.6.0)
|
|
2082
|
+
const [legacyLibraries, setLegacyLibraries] = useState(null);
|
|
2083
|
+
const [uiReady, setUiReady] = useState(useBuiltInUI); // Ready immediately if forcing built-in
|
|
2084
|
+
const [currentNotification, setCurrentNotification] = useState(null);
|
|
1368
2085
|
if (!storageRef.current) {
|
|
1369
2086
|
if (typeof storage === 'string') {
|
|
1370
2087
|
// StorageType enum
|
|
@@ -1446,6 +2163,30 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
|
|
|
1446
2163
|
console.error('Error saving notified achievements', e);
|
|
1447
2164
|
}
|
|
1448
2165
|
};
|
|
2166
|
+
// NEW: Detect legacy UI libraries on mount (v3.6.0)
|
|
2167
|
+
useEffect(() => {
|
|
2168
|
+
if (useBuiltInUI) {
|
|
2169
|
+
// User explicitly wants built-in UI, skip detection
|
|
2170
|
+
setUiReady(true);
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
// Attempt to detect legacy libraries
|
|
2174
|
+
detectLegacyLibraries().then((libs) => {
|
|
2175
|
+
setLegacyLibraries(libs);
|
|
2176
|
+
setUiReady(true);
|
|
2177
|
+
});
|
|
2178
|
+
}, [useBuiltInUI]);
|
|
2179
|
+
// NEW: Resolve UI components based on detection and config (v3.6.0)
|
|
2180
|
+
const NotificationComponent = ui.NotificationComponent ||
|
|
2181
|
+
(useBuiltInUI ? BuiltInNotification :
|
|
2182
|
+
legacyLibraries && Object.keys(legacyLibraries).length > 0 && legacyLibraries.toast
|
|
2183
|
+
? createLegacyToastNotification(legacyLibraries)
|
|
2184
|
+
: BuiltInNotification);
|
|
2185
|
+
const ConfettiComponentResolved = ui.ConfettiComponent ||
|
|
2186
|
+
(useBuiltInUI ? BuiltInConfetti :
|
|
2187
|
+
legacyLibraries && Object.keys(legacyLibraries).length > 0 && legacyLibraries.Confetti
|
|
2188
|
+
? createLegacyConfettiWrapper(legacyLibraries)
|
|
2189
|
+
: BuiltInConfetti);
|
|
1449
2190
|
useEffect(() => {
|
|
1450
2191
|
if (!initialLoadRef.current) {
|
|
1451
2192
|
const savedUnlocked = storageImpl.getUnlockedAchievements() || [];
|
|
@@ -1495,30 +2236,21 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
|
|
|
1495
2236
|
const allUnlocked = [...achievementState.unlocked, ...newlyUnlockedAchievements];
|
|
1496
2237
|
setAchievementState(prev => (Object.assign(Object.assign({}, prev), { unlocked: allUnlocked })));
|
|
1497
2238
|
storageImpl.setUnlockedAchievements(allUnlocked);
|
|
1498
|
-
if (achievementToShow) {
|
|
2239
|
+
if (achievementToShow && (ui.enableNotifications !== false)) {
|
|
1499
2240
|
const achievement = achievementToShow;
|
|
1500
|
-
//
|
|
2241
|
+
// Get icon to display
|
|
1501
2242
|
let iconToDisplay = '🏆';
|
|
1502
2243
|
if (achievement.achievementIconKey && achievement.achievementIconKey in icons) {
|
|
1503
2244
|
iconToDisplay = icons[achievement.achievementIconKey];
|
|
1504
2245
|
}
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
:
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
React.createElement("div", { style: { color: '#666' } }, achievement.achievementDescription))), {
|
|
1514
|
-
position: "top-right",
|
|
1515
|
-
autoClose: 5000,
|
|
1516
|
-
hideProgressBar: false,
|
|
1517
|
-
closeOnClick: true,
|
|
1518
|
-
pauseOnHover: true,
|
|
1519
|
-
draggable: true,
|
|
1520
|
-
progress: undefined,
|
|
1521
|
-
toastId
|
|
2246
|
+
// NEW: Use resolved notification component (v3.6.0)
|
|
2247
|
+
setCurrentNotification({
|
|
2248
|
+
achievement: {
|
|
2249
|
+
id: achievement.achievementId || `achievement-${Date.now()}`,
|
|
2250
|
+
title: achievement.achievementTitle || 'Achievement Unlocked!',
|
|
2251
|
+
description: achievement.achievementDescription || '',
|
|
2252
|
+
icon: iconToDisplay,
|
|
2253
|
+
},
|
|
1522
2254
|
});
|
|
1523
2255
|
// Update seen achievements
|
|
1524
2256
|
if (achievement.achievementId) {
|
|
@@ -1643,8 +2375,8 @@ const AchievementProvider = ({ achievements: achievementsConfig, storage = Stora
|
|
|
1643
2375
|
getAllAchievements,
|
|
1644
2376
|
} },
|
|
1645
2377
|
children,
|
|
1646
|
-
React.createElement(
|
|
1647
|
-
React.createElement(
|
|
2378
|
+
uiReady && currentNotification && ui.enableNotifications !== false && (React.createElement(NotificationComponent, { achievement: currentNotification.achievement, onClose: () => setCurrentNotification(null), duration: 5000, position: ui.notificationPosition || 'top-center', theme: ui.theme || 'modern' })),
|
|
2379
|
+
uiReady && ui.enableConfetti !== false && (React.createElement(ConfettiComponentResolved, { show: showConfetti, duration: 5000 }))));
|
|
1648
2380
|
};
|
|
1649
2381
|
|
|
1650
2382
|
const useAchievements = () => {
|
|
@@ -2011,5 +2743,5 @@ class AchievementBuilder {
|
|
|
2011
2743
|
}
|
|
2012
2744
|
}
|
|
2013
2745
|
|
|
2014
|
-
export { AchievementBuilder, AchievementContext, AchievementError, AchievementProvider, AsyncStorageAdapter, BadgesButton, BadgesModal, ConfettiWrapper, ConfigurationError, ImportValidationError, IndexedDBStorage, LocalStorage, MemoryStorage, OfflineQueueStorage, RestApiStorage, StorageError, StorageQuotaError, StorageType, SyncError, createConfigHash, defaultAchievementIcons, defaultStyles, exportAchievementData, importAchievementData, isAchievementError, isAsyncStorage, isRecoverableError, isSimpleConfig, normalizeAchievements, useAchievements, useSimpleAchievements };
|
|
2746
|
+
export { AchievementBuilder, AchievementContext, AchievementError, AchievementProvider, AsyncStorageAdapter, BadgesButton, BadgesModal, BuiltInConfetti, BuiltInModal, BuiltInNotification, ConfettiWrapper, ConfigurationError, ImportValidationError, IndexedDBStorage, LocalStorage, MemoryStorage, OfflineQueueStorage, RestApiStorage, StorageError, StorageQuotaError, StorageType, SyncError, createConfigHash, defaultAchievementIcons, defaultStyles, exportAchievementData, importAchievementData, isAchievementError, isAsyncStorage, isRecoverableError, isSimpleConfig, normalizeAchievements, useAchievements, useSimpleAchievements, useWindowSize };
|
|
2015
2747
|
//# sourceMappingURL=index.js.map
|