@papernote/ui 1.10.12 → 1.10.13
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/dist/components/AchievementBadge.d.ts +37 -0
- package/dist/components/AchievementBadge.d.ts.map +1 -0
- package/dist/components/AchievementUnlock.d.ts +31 -0
- package/dist/components/AchievementUnlock.d.ts.map +1 -0
- package/dist/components/ActivityFeed.d.ts +42 -0
- package/dist/components/ActivityFeed.d.ts.map +1 -0
- package/dist/components/CollaboratorAvatars.d.ts +33 -0
- package/dist/components/CollaboratorAvatars.d.ts.map +1 -0
- package/dist/components/InviteCard.d.ts +33 -0
- package/dist/components/InviteCard.d.ts.map +1 -0
- package/dist/components/MotivationalMessage.d.ts +31 -0
- package/dist/components/MotivationalMessage.d.ts.map +1 -0
- package/dist/components/PermissionBadge.d.ts +25 -0
- package/dist/components/PermissionBadge.d.ts.map +1 -0
- package/dist/components/ProgressCelebration.d.ts +30 -0
- package/dist/components/ProgressCelebration.d.ts.map +1 -0
- package/dist/components/SharedBadge.d.ts +28 -0
- package/dist/components/SharedBadge.d.ts.map +1 -0
- package/dist/components/StreakBadge.d.ts +27 -0
- package/dist/components/StreakBadge.d.ts.map +1 -0
- package/dist/components/SuccessCheck.d.ts +27 -0
- package/dist/components/SuccessCheck.d.ts.map +1 -0
- package/dist/components/index.d.ts +24 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/hooks/useDelighters.d.ts +55 -0
- package/dist/hooks/useDelighters.d.ts.map +1 -0
- package/dist/index.d.ts +382 -2
- package/dist/index.esm.js +1385 -486
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1395 -484
- package/dist/index.js.map +1 -1
- package/dist/styles.css +201 -0
- package/package.json +1 -1
- package/src/components/AchievementBadge.stories.tsx +290 -0
- package/src/components/AchievementBadge.tsx +196 -0
- package/src/components/AchievementUnlock.stories.tsx +345 -0
- package/src/components/AchievementUnlock.tsx +157 -0
- package/src/components/ActivityFeed.stories.tsx +236 -0
- package/src/components/ActivityFeed.tsx +160 -0
- package/src/components/Celebration.stories.tsx +3 -3
- package/src/components/CollaboratorAvatars.stories.tsx +215 -0
- package/src/components/CollaboratorAvatars.tsx +175 -0
- package/src/components/InviteCard.stories.tsx +174 -0
- package/src/components/InviteCard.tsx +209 -0
- package/src/components/MotivationalMessage.stories.tsx +258 -0
- package/src/components/MotivationalMessage.tsx +120 -0
- package/src/components/PermissionBadge.stories.tsx +208 -0
- package/src/components/PermissionBadge.tsx +204 -0
- package/src/components/ProgressCelebration.stories.tsx +321 -0
- package/src/components/ProgressCelebration.tsx +143 -0
- package/src/components/SharedBadge.stories.tsx +210 -0
- package/src/components/SharedBadge.tsx +111 -0
- package/src/components/StreakBadge.stories.tsx +222 -0
- package/src/components/StreakBadge.tsx +132 -0
- package/src/components/SuccessCheck.stories.tsx +233 -0
- package/src/components/SuccessCheck.tsx +214 -0
- package/src/components/index.ts +38 -0
- package/src/hooks/useDelighters.ts +133 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { AchievementUnlock } from './AchievementUnlock';
|
|
4
|
+
import { Button } from './Button';
|
|
5
|
+
import { Stack } from './Stack';
|
|
6
|
+
import { Text } from './Text';
|
|
7
|
+
import {
|
|
8
|
+
Trophy,
|
|
9
|
+
Star,
|
|
10
|
+
Target,
|
|
11
|
+
Wallet,
|
|
12
|
+
PiggyBank,
|
|
13
|
+
TrendingUp,
|
|
14
|
+
Calendar,
|
|
15
|
+
Award,
|
|
16
|
+
Zap,
|
|
17
|
+
Crown,
|
|
18
|
+
} from 'lucide-react';
|
|
19
|
+
|
|
20
|
+
const meta: Meta<typeof AchievementUnlock> = {
|
|
21
|
+
title: 'Feedback/AchievementUnlock',
|
|
22
|
+
component: AchievementUnlock,
|
|
23
|
+
parameters: {
|
|
24
|
+
layout: 'centered',
|
|
25
|
+
docs: {
|
|
26
|
+
description: {
|
|
27
|
+
component: 'Modal for newly unlocked achievements. Combines Modal + Celebration + AchievementBadge with configurable celebration types and optional auto-close.',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
tags: ['autodocs'],
|
|
32
|
+
argTypes: {
|
|
33
|
+
celebrationType: {
|
|
34
|
+
control: 'select',
|
|
35
|
+
options: ['confetti', 'glow', 'bounce'],
|
|
36
|
+
description: 'Type of celebration animation',
|
|
37
|
+
},
|
|
38
|
+
autoClose: {
|
|
39
|
+
control: 'boolean',
|
|
40
|
+
description: 'Whether to auto-close after a delay',
|
|
41
|
+
},
|
|
42
|
+
autoCloseDelay: {
|
|
43
|
+
control: { type: 'number', min: 1000, max: 10000, step: 500 },
|
|
44
|
+
description: 'Delay before auto-close in ms',
|
|
45
|
+
},
|
|
46
|
+
enabled: {
|
|
47
|
+
control: 'boolean',
|
|
48
|
+
description: 'Whether celebrations are enabled',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default meta;
|
|
54
|
+
type Story = StoryObj<typeof AchievementUnlock>;
|
|
55
|
+
|
|
56
|
+
// Basic example with trigger button
|
|
57
|
+
export const Default: Story = {
|
|
58
|
+
render: function DefaultDemo() {
|
|
59
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Stack align="center" gap="md">
|
|
63
|
+
<Button onClick={() => setIsOpen(true)}>
|
|
64
|
+
Unlock Achievement
|
|
65
|
+
</Button>
|
|
66
|
+
<AchievementUnlock
|
|
67
|
+
isOpen={isOpen}
|
|
68
|
+
onClose={() => setIsOpen(false)}
|
|
69
|
+
badge={{
|
|
70
|
+
icon: <Trophy className="w-full h-full" />,
|
|
71
|
+
name: 'Budget Master',
|
|
72
|
+
description: 'You stayed under budget for 3 consecutive months. Keep up the great work!',
|
|
73
|
+
}}
|
|
74
|
+
celebrationType="confetti"
|
|
75
|
+
/>
|
|
76
|
+
</Stack>
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Different celebration types
|
|
82
|
+
export const CelebrationTypes: Story = {
|
|
83
|
+
render: function CelebrationTypesDemo() {
|
|
84
|
+
const [openType, setOpenType] = useState<'confetti' | 'glow' | 'bounce' | null>(null);
|
|
85
|
+
|
|
86
|
+
const badge = {
|
|
87
|
+
icon: <Star className="w-full h-full" />,
|
|
88
|
+
name: 'First Steps',
|
|
89
|
+
description: 'You connected your first bank account and started your financial journey!',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<Stack direction="horizontal" gap="md">
|
|
94
|
+
<Button onClick={() => setOpenType('confetti')}>Confetti</Button>
|
|
95
|
+
<Button onClick={() => setOpenType('glow')}>Glow</Button>
|
|
96
|
+
<Button onClick={() => setOpenType('bounce')}>Bounce</Button>
|
|
97
|
+
|
|
98
|
+
{openType && (
|
|
99
|
+
<AchievementUnlock
|
|
100
|
+
isOpen={true}
|
|
101
|
+
onClose={() => setOpenType(null)}
|
|
102
|
+
badge={badge}
|
|
103
|
+
celebrationType={openType}
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
106
|
+
</Stack>
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Auto-close
|
|
112
|
+
export const AutoClose: Story = {
|
|
113
|
+
render: function AutoCloseDemo() {
|
|
114
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Stack align="center" gap="md">
|
|
118
|
+
<Button onClick={() => setIsOpen(true)}>
|
|
119
|
+
Unlock (Auto-closes in 3s)
|
|
120
|
+
</Button>
|
|
121
|
+
<AchievementUnlock
|
|
122
|
+
isOpen={isOpen}
|
|
123
|
+
onClose={() => setIsOpen(false)}
|
|
124
|
+
badge={{
|
|
125
|
+
icon: <Zap className="w-full h-full" />,
|
|
126
|
+
name: 'Quick Start',
|
|
127
|
+
description: 'You completed the onboarding in record time!',
|
|
128
|
+
}}
|
|
129
|
+
autoClose
|
|
130
|
+
autoCloseDelay={3000}
|
|
131
|
+
/>
|
|
132
|
+
</Stack>
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Various achievements showcase
|
|
138
|
+
export const AchievementShowcase: Story = {
|
|
139
|
+
render: function AchievementShowcaseDemo() {
|
|
140
|
+
const [currentAchievement, setCurrentAchievement] = useState<number | null>(null);
|
|
141
|
+
|
|
142
|
+
const achievements = [
|
|
143
|
+
{
|
|
144
|
+
icon: <Star className="w-full h-full" />,
|
|
145
|
+
name: 'First Steps',
|
|
146
|
+
description: 'Connected your first bank account',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
icon: <Wallet className="w-full h-full" />,
|
|
150
|
+
name: 'Budget Creator',
|
|
151
|
+
description: 'Created your first monthly budget',
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
icon: <Trophy className="w-full h-full" />,
|
|
155
|
+
name: 'Budget Master',
|
|
156
|
+
description: 'Stayed under budget for 3 consecutive months',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
icon: <PiggyBank className="w-full h-full" />,
|
|
160
|
+
name: 'Super Saver',
|
|
161
|
+
description: 'Saved $1,000 in a single month',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
icon: <TrendingUp className="w-full h-full" />,
|
|
165
|
+
name: 'Investor',
|
|
166
|
+
description: 'Started tracking your investment portfolio',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
icon: <Calendar className="w-full h-full" />,
|
|
170
|
+
name: '30-Day Streak',
|
|
171
|
+
description: 'Tracked your finances for 30 days straight',
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
icon: <Award className="w-full h-full" />,
|
|
175
|
+
name: 'Tax Pro',
|
|
176
|
+
description: 'Categorized all expenses for tax season',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
icon: <Crown className="w-full h-full" />,
|
|
180
|
+
name: 'Finance King',
|
|
181
|
+
description: 'Reached $100,000 in total savings',
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<Stack gap="lg">
|
|
187
|
+
<Text weight="semibold">Click an achievement to see the unlock modal:</Text>
|
|
188
|
+
<div className="grid grid-cols-4 gap-4">
|
|
189
|
+
{achievements.map((achievement, index) => (
|
|
190
|
+
<Button
|
|
191
|
+
key={index}
|
|
192
|
+
variant="ghost"
|
|
193
|
+
onClick={() => setCurrentAchievement(index)}
|
|
194
|
+
className="flex flex-col items-center gap-2 p-4 h-auto"
|
|
195
|
+
>
|
|
196
|
+
<div className="w-10 h-10 text-accent-500">
|
|
197
|
+
{achievement.icon}
|
|
198
|
+
</div>
|
|
199
|
+
<Text size="xs" className="text-center">{achievement.name}</Text>
|
|
200
|
+
</Button>
|
|
201
|
+
))}
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
{currentAchievement !== null && (
|
|
205
|
+
<AchievementUnlock
|
|
206
|
+
isOpen={true}
|
|
207
|
+
onClose={() => setCurrentAchievement(null)}
|
|
208
|
+
badge={achievements[currentAchievement]}
|
|
209
|
+
celebrationType="confetti"
|
|
210
|
+
/>
|
|
211
|
+
)}
|
|
212
|
+
</Stack>
|
|
213
|
+
);
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Disabled celebrations (accessibility)
|
|
218
|
+
export const DisabledCelebrations: Story = {
|
|
219
|
+
render: function DisabledDemo() {
|
|
220
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<Stack align="center" gap="md">
|
|
224
|
+
<Button onClick={() => setIsOpen(true)}>
|
|
225
|
+
Unlock (No Animations)
|
|
226
|
+
</Button>
|
|
227
|
+
<Text size="sm" className="text-ink-500">
|
|
228
|
+
When enabled=false, no confetti or animations appear.
|
|
229
|
+
</Text>
|
|
230
|
+
<AchievementUnlock
|
|
231
|
+
isOpen={isOpen}
|
|
232
|
+
onClose={() => setIsOpen(false)}
|
|
233
|
+
badge={{
|
|
234
|
+
icon: <Target className="w-full h-full" />,
|
|
235
|
+
name: 'Goal Setter',
|
|
236
|
+
description: 'Set and tracked 5 financial goals',
|
|
237
|
+
}}
|
|
238
|
+
enabled={false}
|
|
239
|
+
/>
|
|
240
|
+
</Stack>
|
|
241
|
+
);
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// Programmatic unlock simulation
|
|
246
|
+
export const ProgrammaticUnlock: Story = {
|
|
247
|
+
render: function ProgrammaticDemo() {
|
|
248
|
+
const [progress, setProgress] = useState(0);
|
|
249
|
+
const [unlockedAchievement, setUnlockedAchievement] = useState<{
|
|
250
|
+
icon: React.ReactNode;
|
|
251
|
+
name: string;
|
|
252
|
+
description: string;
|
|
253
|
+
} | null>(null);
|
|
254
|
+
|
|
255
|
+
const handleIncrement = () => {
|
|
256
|
+
const newProgress = Math.min(100, progress + 20);
|
|
257
|
+
setProgress(newProgress);
|
|
258
|
+
|
|
259
|
+
// Simulate unlocking an achievement at 100%
|
|
260
|
+
if (newProgress >= 100 && progress < 100) {
|
|
261
|
+
setUnlockedAchievement({
|
|
262
|
+
icon: <Trophy className="w-full h-full" />,
|
|
263
|
+
name: 'Goal Achieved!',
|
|
264
|
+
description: 'You reached 100% of your savings goal. Amazing work!',
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<Stack align="center" gap="lg" className="w-64">
|
|
271
|
+
<Text weight="semibold">Savings Goal Progress</Text>
|
|
272
|
+
<div className="w-full bg-paper-200 rounded-full h-4 overflow-hidden">
|
|
273
|
+
<div
|
|
274
|
+
className="bg-success-500 h-full transition-all duration-300"
|
|
275
|
+
style={{ width: `${progress}%` }}
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
<Text size="sm" className="text-ink-500">{progress}% complete</Text>
|
|
279
|
+
|
|
280
|
+
<Stack direction="horizontal" gap="sm">
|
|
281
|
+
<Button onClick={handleIncrement} disabled={progress >= 100}>
|
|
282
|
+
Add 20%
|
|
283
|
+
</Button>
|
|
284
|
+
<Button variant="ghost" onClick={() => setProgress(0)}>
|
|
285
|
+
Reset
|
|
286
|
+
</Button>
|
|
287
|
+
</Stack>
|
|
288
|
+
|
|
289
|
+
{unlockedAchievement && (
|
|
290
|
+
<AchievementUnlock
|
|
291
|
+
isOpen={true}
|
|
292
|
+
onClose={() => setUnlockedAchievement(null)}
|
|
293
|
+
badge={unlockedAchievement}
|
|
294
|
+
celebrationType="confetti"
|
|
295
|
+
/>
|
|
296
|
+
)}
|
|
297
|
+
</Stack>
|
|
298
|
+
);
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// Multiple sequential achievements
|
|
303
|
+
export const SequentialAchievements: Story = {
|
|
304
|
+
render: function SequentialDemo() {
|
|
305
|
+
const [queue, setQueue] = useState<Array<{
|
|
306
|
+
icon: React.ReactNode;
|
|
307
|
+
name: string;
|
|
308
|
+
description: string;
|
|
309
|
+
}>>([]);
|
|
310
|
+
|
|
311
|
+
const achievements = [
|
|
312
|
+
{ icon: <Star className="w-full h-full" />, name: 'First Steps', description: 'Welcome to your financial journey!' },
|
|
313
|
+
{ icon: <Wallet className="w-full h-full" />, name: 'Budget Creator', description: 'You created your first budget!' },
|
|
314
|
+
{ icon: <Trophy className="w-full h-full" />, name: 'Champion', description: 'You completed the challenge!' },
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
const triggerMultiple = () => {
|
|
318
|
+
setQueue([...achievements]);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const handleClose = () => {
|
|
322
|
+
setQueue((q) => q.slice(1));
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<Stack align="center" gap="md">
|
|
327
|
+
<Button onClick={triggerMultiple}>
|
|
328
|
+
Unlock 3 Achievements
|
|
329
|
+
</Button>
|
|
330
|
+
<Text size="sm" className="text-ink-500">
|
|
331
|
+
{queue.length > 0 ? `${queue.length} achievements remaining` : 'Click to start'}
|
|
332
|
+
</Text>
|
|
333
|
+
|
|
334
|
+
{queue.length > 0 && (
|
|
335
|
+
<AchievementUnlock
|
|
336
|
+
isOpen={true}
|
|
337
|
+
onClose={handleClose}
|
|
338
|
+
badge={queue[0]}
|
|
339
|
+
celebrationType="confetti"
|
|
340
|
+
/>
|
|
341
|
+
)}
|
|
342
|
+
</Stack>
|
|
343
|
+
);
|
|
344
|
+
},
|
|
345
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import Modal from './Modal';
|
|
3
|
+
import { useCelebration } from './Celebration';
|
|
4
|
+
import { AchievementBadge, AchievementBadgeData } from './AchievementBadge';
|
|
5
|
+
import Button from './Button';
|
|
6
|
+
import Stack from './Stack';
|
|
7
|
+
import Text from './Text';
|
|
8
|
+
|
|
9
|
+
export interface AchievementUnlockProps {
|
|
10
|
+
/** Whether the modal is open */
|
|
11
|
+
isOpen: boolean;
|
|
12
|
+
/** Callback when the modal is closed */
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
/** The badge data for the achievement */
|
|
15
|
+
badge: AchievementBadgeData;
|
|
16
|
+
/** Type of celebration animation */
|
|
17
|
+
celebrationType?: 'confetti' | 'glow' | 'bounce';
|
|
18
|
+
/** Whether to auto-close after a delay */
|
|
19
|
+
autoClose?: boolean;
|
|
20
|
+
/** Delay before auto-close in ms (default: 5000) */
|
|
21
|
+
autoCloseDelay?: number;
|
|
22
|
+
/** Whether celebrations are enabled */
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* AchievementUnlock - Modal/toast for newly unlocked achievements.
|
|
28
|
+
*
|
|
29
|
+
* Features:
|
|
30
|
+
* - Composes Modal + Celebration + AchievementBadge
|
|
31
|
+
* - Center the badge with scale-in animation
|
|
32
|
+
* - Triggers celebration effect on open
|
|
33
|
+
* - "Awesome!" dismiss button
|
|
34
|
+
* - Optional auto-close after delay
|
|
35
|
+
* - Mobile: uses BottomSheet via Modal's adaptive behavior
|
|
36
|
+
*/
|
|
37
|
+
export function AchievementUnlock({
|
|
38
|
+
isOpen,
|
|
39
|
+
onClose,
|
|
40
|
+
badge,
|
|
41
|
+
celebrationType = 'confetti',
|
|
42
|
+
autoClose = false,
|
|
43
|
+
autoCloseDelay = 5000,
|
|
44
|
+
enabled = true,
|
|
45
|
+
}: AchievementUnlockProps) {
|
|
46
|
+
const { celebrate } = useCelebration();
|
|
47
|
+
const autoCloseTimeoutRef = useRef<number | null>(null);
|
|
48
|
+
const hasTriggeredRef = useRef(false);
|
|
49
|
+
|
|
50
|
+
// Check for reduced motion preference
|
|
51
|
+
const prefersReducedMotion =
|
|
52
|
+
typeof window !== 'undefined' &&
|
|
53
|
+
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
54
|
+
|
|
55
|
+
// Trigger celebration when modal opens
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (isOpen && enabled && !prefersReducedMotion && !hasTriggeredRef.current) {
|
|
58
|
+
hasTriggeredRef.current = true;
|
|
59
|
+
|
|
60
|
+
if (celebrationType === 'confetti') {
|
|
61
|
+
// Delay celebration slightly for visual effect after modal animation
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
celebrate({
|
|
64
|
+
type: 'confetti',
|
|
65
|
+
particleCount: 100,
|
|
66
|
+
duration: 2500,
|
|
67
|
+
colors: ['#22c55e', '#3b82f6', '#a855f7', '#f59e0b', '#ec4899'],
|
|
68
|
+
});
|
|
69
|
+
}, 300);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!isOpen) {
|
|
74
|
+
hasTriggeredRef.current = false;
|
|
75
|
+
}
|
|
76
|
+
}, [isOpen, enabled, prefersReducedMotion, celebrationType, celebrate]);
|
|
77
|
+
|
|
78
|
+
// Auto-close functionality
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (isOpen && autoClose) {
|
|
81
|
+
autoCloseTimeoutRef.current = window.setTimeout(() => {
|
|
82
|
+
onClose();
|
|
83
|
+
}, autoCloseDelay);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
if (autoCloseTimeoutRef.current) {
|
|
88
|
+
clearTimeout(autoCloseTimeoutRef.current);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}, [isOpen, autoClose, autoCloseDelay, onClose]);
|
|
92
|
+
|
|
93
|
+
// Mark badge as earned with current date
|
|
94
|
+
const earnedBadge = {
|
|
95
|
+
...badge,
|
|
96
|
+
earnedAt: badge.earnedAt || new Date(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Modal
|
|
101
|
+
isOpen={isOpen}
|
|
102
|
+
onClose={onClose}
|
|
103
|
+
title="Achievement Unlocked!"
|
|
104
|
+
size="sm"
|
|
105
|
+
animation="scale"
|
|
106
|
+
showCloseButton={false}
|
|
107
|
+
mobileMode="auto"
|
|
108
|
+
mobileHeight="md"
|
|
109
|
+
>
|
|
110
|
+
<Stack align="center" gap="lg" className="py-6">
|
|
111
|
+
{/* Achievement badge with glow/bounce effect */}
|
|
112
|
+
<div
|
|
113
|
+
className={`
|
|
114
|
+
${celebrationType === 'glow' && enabled && !prefersReducedMotion ? 'animate-pulse' : ''}
|
|
115
|
+
${celebrationType === 'bounce' && enabled && !prefersReducedMotion ? 'animate-bounce-subtle' : ''}
|
|
116
|
+
`}
|
|
117
|
+
>
|
|
118
|
+
<AchievementBadge
|
|
119
|
+
badge={earnedBadge}
|
|
120
|
+
variant="earned"
|
|
121
|
+
size="lg"
|
|
122
|
+
showTooltip={false}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Achievement details */}
|
|
127
|
+
<Stack align="center" gap="sm">
|
|
128
|
+
<Text size="xl" weight="bold" className="text-ink-800">
|
|
129
|
+
{badge.name}
|
|
130
|
+
</Text>
|
|
131
|
+
<Text size="sm" className="text-ink-500 text-center max-w-64">
|
|
132
|
+
{badge.description}
|
|
133
|
+
</Text>
|
|
134
|
+
</Stack>
|
|
135
|
+
|
|
136
|
+
{/* Dismiss button */}
|
|
137
|
+
<Button
|
|
138
|
+
onClick={onClose}
|
|
139
|
+
variant="primary"
|
|
140
|
+
size="lg"
|
|
141
|
+
className="min-w-32"
|
|
142
|
+
>
|
|
143
|
+
Awesome!
|
|
144
|
+
</Button>
|
|
145
|
+
|
|
146
|
+
{/* Auto-close indicator */}
|
|
147
|
+
{autoClose && (
|
|
148
|
+
<Text size="xs" className="text-ink-400">
|
|
149
|
+
Closing in {Math.ceil(autoCloseDelay / 1000)} seconds...
|
|
150
|
+
</Text>
|
|
151
|
+
)}
|
|
152
|
+
</Stack>
|
|
153
|
+
</Modal>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default AchievementUnlock;
|