@papernote/ui 1.10.11 → 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/Celebration.d.ts +47 -0
- package/dist/components/Celebration.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 +26 -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 +428 -2
- package/dist/index.esm.js +2471 -486
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2483 -484
- package/dist/index.js.map +1 -1
- package/dist/styles.css +201 -0
- package/package.json +3 -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 +175 -0
- package/src/components/Celebration.tsx +256 -0
- 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 +40 -0
- package/src/hooks/useDelighters.ts +133 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { AchievementBadge } from './AchievementBadge';
|
|
3
|
+
import { Stack } from './Stack';
|
|
4
|
+
import { Text } from './Text';
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from './Card';
|
|
6
|
+
import {
|
|
7
|
+
Trophy,
|
|
8
|
+
Target,
|
|
9
|
+
Wallet,
|
|
10
|
+
PiggyBank,
|
|
11
|
+
TrendingUp,
|
|
12
|
+
Calendar,
|
|
13
|
+
Star,
|
|
14
|
+
Zap,
|
|
15
|
+
Award,
|
|
16
|
+
Crown,
|
|
17
|
+
} from 'lucide-react';
|
|
18
|
+
|
|
19
|
+
const meta: Meta<typeof AchievementBadge> = {
|
|
20
|
+
title: 'Feedback/AchievementBadge',
|
|
21
|
+
component: AchievementBadge,
|
|
22
|
+
parameters: {
|
|
23
|
+
layout: 'centered',
|
|
24
|
+
docs: {
|
|
25
|
+
description: {
|
|
26
|
+
component: 'Display achievement badges with earned/locked/in-progress states. Features circular progress ring, tooltip with details, and visual states for different achievement statuses.',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
tags: ['autodocs'],
|
|
31
|
+
argTypes: {
|
|
32
|
+
variant: {
|
|
33
|
+
control: 'select',
|
|
34
|
+
options: ['earned', 'locked', 'in-progress'],
|
|
35
|
+
description: 'Current state of the achievement',
|
|
36
|
+
},
|
|
37
|
+
progress: {
|
|
38
|
+
control: { type: 'number', min: 0, max: 100, step: 5 },
|
|
39
|
+
description: 'Progress percentage for in-progress variant',
|
|
40
|
+
},
|
|
41
|
+
size: {
|
|
42
|
+
control: 'select',
|
|
43
|
+
options: ['sm', 'md', 'lg'],
|
|
44
|
+
description: 'Size of the badge',
|
|
45
|
+
},
|
|
46
|
+
showTooltip: {
|
|
47
|
+
control: 'boolean',
|
|
48
|
+
description: 'Whether to show tooltip on hover',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export default meta;
|
|
54
|
+
type Story = StoryObj<typeof AchievementBadge>;
|
|
55
|
+
|
|
56
|
+
const sampleBadge = {
|
|
57
|
+
icon: <Trophy className="w-full h-full" />,
|
|
58
|
+
name: 'Budget Master',
|
|
59
|
+
description: 'Stay under budget for 3 consecutive months',
|
|
60
|
+
earnedAt: new Date('2024-03-15'),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Basic examples for each variant
|
|
64
|
+
export const Earned: Story = {
|
|
65
|
+
args: {
|
|
66
|
+
badge: sampleBadge,
|
|
67
|
+
variant: 'earned',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const Locked: Story = {
|
|
72
|
+
args: {
|
|
73
|
+
badge: {
|
|
74
|
+
icon: <Crown className="w-full h-full" />,
|
|
75
|
+
name: 'Finance King',
|
|
76
|
+
description: 'Reach $100,000 in total savings',
|
|
77
|
+
},
|
|
78
|
+
variant: 'locked',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const InProgress: Story = {
|
|
83
|
+
args: {
|
|
84
|
+
badge: {
|
|
85
|
+
icon: <Target className="w-full h-full" />,
|
|
86
|
+
name: 'Goal Setter',
|
|
87
|
+
description: 'Set and track 5 financial goals',
|
|
88
|
+
},
|
|
89
|
+
variant: 'in-progress',
|
|
90
|
+
progress: 60,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// All sizes
|
|
95
|
+
export const Sizes: Story = {
|
|
96
|
+
render: () => (
|
|
97
|
+
<Stack direction="horizontal" gap="lg" align="center">
|
|
98
|
+
<Stack align="center" gap="sm">
|
|
99
|
+
<AchievementBadge badge={sampleBadge} variant="earned" size="sm" />
|
|
100
|
+
<Text size="xs" className="text-ink-400">Small</Text>
|
|
101
|
+
</Stack>
|
|
102
|
+
<Stack align="center" gap="sm">
|
|
103
|
+
<AchievementBadge badge={sampleBadge} variant="earned" size="md" />
|
|
104
|
+
<Text size="xs" className="text-ink-400">Medium</Text>
|
|
105
|
+
</Stack>
|
|
106
|
+
<Stack align="center" gap="sm">
|
|
107
|
+
<AchievementBadge badge={sampleBadge} variant="earned" size="lg" />
|
|
108
|
+
<Text size="xs" className="text-ink-400">Large</Text>
|
|
109
|
+
</Stack>
|
|
110
|
+
</Stack>
|
|
111
|
+
),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// Progress levels
|
|
115
|
+
export const ProgressLevels: Story = {
|
|
116
|
+
render: () => (
|
|
117
|
+
<Stack direction="horizontal" gap="md" align="center">
|
|
118
|
+
{[0, 25, 50, 75, 100].map((progress) => (
|
|
119
|
+
<Stack key={progress} align="center" gap="sm">
|
|
120
|
+
<AchievementBadge
|
|
121
|
+
badge={{
|
|
122
|
+
icon: <Target className="w-full h-full" />,
|
|
123
|
+
name: 'Goal Setter',
|
|
124
|
+
description: 'Set and track 5 financial goals',
|
|
125
|
+
}}
|
|
126
|
+
variant={progress === 100 ? 'earned' : 'in-progress'}
|
|
127
|
+
progress={progress}
|
|
128
|
+
/>
|
|
129
|
+
<Text size="xs" className="text-ink-400">{progress}%</Text>
|
|
130
|
+
</Stack>
|
|
131
|
+
))}
|
|
132
|
+
</Stack>
|
|
133
|
+
),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Collection of badges
|
|
137
|
+
export const BadgeCollection: Story = {
|
|
138
|
+
render: () => {
|
|
139
|
+
const badges = [
|
|
140
|
+
{
|
|
141
|
+
badge: { icon: <Star className="w-full h-full" />, name: 'First Steps', description: 'Connect your first bank account', earnedAt: new Date('2024-01-10') },
|
|
142
|
+
variant: 'earned' as const,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
badge: { icon: <Wallet className="w-full h-full" />, name: 'Budget Beginner', description: 'Create your first budget', earnedAt: new Date('2024-01-15') },
|
|
146
|
+
variant: 'earned' as const,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
badge: { icon: <Trophy className="w-full h-full" />, name: 'Budget Master', description: 'Stay under budget for 3 months', earnedAt: new Date('2024-03-15') },
|
|
150
|
+
variant: 'earned' as const,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
badge: { icon: <PiggyBank className="w-full h-full" />, name: 'Super Saver', description: 'Save $1,000 in a month' },
|
|
154
|
+
variant: 'in-progress' as const,
|
|
155
|
+
progress: 75,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
badge: { icon: <TrendingUp className="w-full h-full" />, name: 'Investor', description: 'Track investment portfolio' },
|
|
159
|
+
variant: 'in-progress' as const,
|
|
160
|
+
progress: 30,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
badge: { icon: <Crown className="w-full h-full" />, name: 'Finance King', description: 'Reach $100k in savings' },
|
|
164
|
+
variant: 'locked' as const,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<div className="grid grid-cols-3 gap-6">
|
|
170
|
+
{badges.map((item, index) => (
|
|
171
|
+
<Stack key={index} align="center" gap="sm">
|
|
172
|
+
<AchievementBadge {...item} />
|
|
173
|
+
<Text size="xs" className="text-ink-500 text-center max-w-20">
|
|
174
|
+
{item.badge.name}
|
|
175
|
+
</Text>
|
|
176
|
+
</Stack>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Profile card context
|
|
184
|
+
export const ProfileContext: Story = {
|
|
185
|
+
render: () => (
|
|
186
|
+
<Card className="w-96">
|
|
187
|
+
<CardHeader>
|
|
188
|
+
<CardTitle>Achievements</CardTitle>
|
|
189
|
+
</CardHeader>
|
|
190
|
+
<CardContent>
|
|
191
|
+
<Stack gap="md">
|
|
192
|
+
<Stack direction="horizontal" gap="sm" justify="center">
|
|
193
|
+
<AchievementBadge
|
|
194
|
+
badge={{ icon: <Star className="w-full h-full" />, name: 'First Steps', description: 'Connect your first account', earnedAt: new Date() }}
|
|
195
|
+
variant="earned"
|
|
196
|
+
size="sm"
|
|
197
|
+
/>
|
|
198
|
+
<AchievementBadge
|
|
199
|
+
badge={{ icon: <Trophy className="w-full h-full" />, name: 'Budget Master', description: 'Stay under budget 3 months', earnedAt: new Date() }}
|
|
200
|
+
variant="earned"
|
|
201
|
+
size="sm"
|
|
202
|
+
/>
|
|
203
|
+
<AchievementBadge
|
|
204
|
+
badge={{ icon: <Zap className="w-full h-full" />, name: 'Quick Start', description: 'Complete onboarding', earnedAt: new Date() }}
|
|
205
|
+
variant="earned"
|
|
206
|
+
size="sm"
|
|
207
|
+
/>
|
|
208
|
+
<AchievementBadge
|
|
209
|
+
badge={{ icon: <Calendar className="w-full h-full" />, name: '30-Day Streak', description: 'Track for 30 days' }}
|
|
210
|
+
variant="in-progress"
|
|
211
|
+
progress={45}
|
|
212
|
+
size="sm"
|
|
213
|
+
/>
|
|
214
|
+
<AchievementBadge
|
|
215
|
+
badge={{ icon: <Award className="w-full h-full" />, name: 'Tax Pro', description: 'Categorize all expenses' }}
|
|
216
|
+
variant="locked"
|
|
217
|
+
size="sm"
|
|
218
|
+
/>
|
|
219
|
+
</Stack>
|
|
220
|
+
<Stack direction="horizontal" justify="between" className="pt-2 border-t border-paper-200">
|
|
221
|
+
<Text size="sm" className="text-ink-500">Total Earned</Text>
|
|
222
|
+
<Text size="sm" weight="semibold" className="text-ink-700">3 of 12</Text>
|
|
223
|
+
</Stack>
|
|
224
|
+
</Stack>
|
|
225
|
+
</CardContent>
|
|
226
|
+
</Card>
|
|
227
|
+
),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Without tooltip
|
|
231
|
+
export const WithoutTooltip: Story = {
|
|
232
|
+
args: {
|
|
233
|
+
badge: sampleBadge,
|
|
234
|
+
variant: 'earned',
|
|
235
|
+
showTooltip: false,
|
|
236
|
+
},
|
|
237
|
+
parameters: {
|
|
238
|
+
docs: {
|
|
239
|
+
description: {
|
|
240
|
+
story: 'When `showTooltip` is false, hovering over the badge does not show the tooltip.',
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Showcase with labels
|
|
247
|
+
export const ShowcaseWithLabels: Story = {
|
|
248
|
+
render: () => (
|
|
249
|
+
<Stack gap="xl">
|
|
250
|
+
<Stack direction="horizontal" gap="lg" align="start">
|
|
251
|
+
<Stack align="center" gap="md" className="p-6 bg-paper-50 rounded-xl">
|
|
252
|
+
<AchievementBadge
|
|
253
|
+
badge={{ icon: <Trophy className="w-full h-full" />, name: 'Budget Master', description: 'Stay under budget for 3 months', earnedAt: new Date() }}
|
|
254
|
+
variant="earned"
|
|
255
|
+
size="lg"
|
|
256
|
+
/>
|
|
257
|
+
<Stack align="center" gap="xs">
|
|
258
|
+
<Text weight="semibold" className="text-ink-700">Budget Master</Text>
|
|
259
|
+
<Text size="sm" className="text-ink-500">Earned Mar 15, 2024</Text>
|
|
260
|
+
</Stack>
|
|
261
|
+
</Stack>
|
|
262
|
+
|
|
263
|
+
<Stack align="center" gap="md" className="p-6 bg-paper-50 rounded-xl">
|
|
264
|
+
<AchievementBadge
|
|
265
|
+
badge={{ icon: <PiggyBank className="w-full h-full" />, name: 'Super Saver', description: 'Save $1,000 in a month' }}
|
|
266
|
+
variant="in-progress"
|
|
267
|
+
progress={75}
|
|
268
|
+
size="lg"
|
|
269
|
+
/>
|
|
270
|
+
<Stack align="center" gap="xs">
|
|
271
|
+
<Text weight="semibold" className="text-ink-700">Super Saver</Text>
|
|
272
|
+
<Text size="sm" className="text-ink-500">75% complete</Text>
|
|
273
|
+
</Stack>
|
|
274
|
+
</Stack>
|
|
275
|
+
|
|
276
|
+
<Stack align="center" gap="md" className="p-6 bg-paper-50 rounded-xl">
|
|
277
|
+
<AchievementBadge
|
|
278
|
+
badge={{ icon: <Crown className="w-full h-full" />, name: 'Finance King', description: 'Reach $100k in savings' }}
|
|
279
|
+
variant="locked"
|
|
280
|
+
size="lg"
|
|
281
|
+
/>
|
|
282
|
+
<Stack align="center" gap="xs">
|
|
283
|
+
<Text weight="semibold" className="text-ink-400">Finance King</Text>
|
|
284
|
+
<Text size="sm" className="text-ink-400">Locked</Text>
|
|
285
|
+
</Stack>
|
|
286
|
+
</Stack>
|
|
287
|
+
</Stack>
|
|
288
|
+
</Stack>
|
|
289
|
+
),
|
|
290
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Tooltip from './Tooltip';
|
|
3
|
+
|
|
4
|
+
export interface AchievementBadgeData {
|
|
5
|
+
/** Icon to display (React node, typically from lucide-react) */
|
|
6
|
+
icon: React.ReactNode;
|
|
7
|
+
/** Name of the achievement */
|
|
8
|
+
name: string;
|
|
9
|
+
/** Description of how to earn this achievement */
|
|
10
|
+
description: string;
|
|
11
|
+
/** When the achievement was earned (undefined if not earned) */
|
|
12
|
+
earnedAt?: Date;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface AchievementBadgeProps {
|
|
16
|
+
/** The badge data */
|
|
17
|
+
badge: AchievementBadgeData;
|
|
18
|
+
/** Current state of the achievement */
|
|
19
|
+
variant: 'earned' | 'locked' | 'in-progress';
|
|
20
|
+
/** Progress percentage (0-100) for in-progress variant */
|
|
21
|
+
progress?: number;
|
|
22
|
+
/** Size of the badge */
|
|
23
|
+
size?: 'sm' | 'md' | 'lg';
|
|
24
|
+
/** Whether to show tooltip on hover */
|
|
25
|
+
showTooltip?: boolean;
|
|
26
|
+
/** Additional CSS classes */
|
|
27
|
+
className?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const sizeStyles = {
|
|
31
|
+
sm: {
|
|
32
|
+
container: 'w-12 h-12',
|
|
33
|
+
iconContainer: 'w-10 h-10',
|
|
34
|
+
icon: 'w-5 h-5',
|
|
35
|
+
ring: 40,
|
|
36
|
+
ringStroke: 3,
|
|
37
|
+
},
|
|
38
|
+
md: {
|
|
39
|
+
container: 'w-16 h-16',
|
|
40
|
+
iconContainer: 'w-14 h-14',
|
|
41
|
+
icon: 'w-7 h-7',
|
|
42
|
+
ring: 56,
|
|
43
|
+
ringStroke: 3,
|
|
44
|
+
},
|
|
45
|
+
lg: {
|
|
46
|
+
container: 'w-20 h-20',
|
|
47
|
+
iconContainer: 'w-18 h-18',
|
|
48
|
+
icon: 'w-9 h-9',
|
|
49
|
+
ring: 72,
|
|
50
|
+
ringStroke: 4,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* AchievementBadge - Display achievement badges with earned/locked/in-progress states.
|
|
56
|
+
*
|
|
57
|
+
* Features:
|
|
58
|
+
* - Three variants: earned (full color + glow), locked (grayscale), in-progress (with ring)
|
|
59
|
+
* - Circular progress ring for in-progress state
|
|
60
|
+
* - Optional tooltip showing name, description, and earned date
|
|
61
|
+
* - Three sizes: sm, md, lg
|
|
62
|
+
*/
|
|
63
|
+
export function AchievementBadge({
|
|
64
|
+
badge,
|
|
65
|
+
variant,
|
|
66
|
+
progress = 0,
|
|
67
|
+
size = 'md',
|
|
68
|
+
showTooltip = true,
|
|
69
|
+
className = '',
|
|
70
|
+
}: AchievementBadgeProps) {
|
|
71
|
+
const styles = sizeStyles[size];
|
|
72
|
+
const clampedProgress = Math.min(100, Math.max(0, progress));
|
|
73
|
+
|
|
74
|
+
// Calculate progress ring
|
|
75
|
+
const ringSize = styles.ring;
|
|
76
|
+
const ringRadius = (ringSize - styles.ringStroke) / 2;
|
|
77
|
+
const ringCircumference = 2 * Math.PI * ringRadius;
|
|
78
|
+
const ringOffset = ringCircumference - (clampedProgress / 100) * ringCircumference;
|
|
79
|
+
|
|
80
|
+
const variantStyles = {
|
|
81
|
+
earned: 'bg-success-100 text-success-600 shadow-md',
|
|
82
|
+
locked: 'bg-paper-200 text-ink-300 grayscale opacity-60',
|
|
83
|
+
'in-progress': 'bg-accent-100 text-accent-600',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const formatDate = (date: Date) => {
|
|
87
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
88
|
+
month: 'short',
|
|
89
|
+
day: 'numeric',
|
|
90
|
+
year: 'numeric',
|
|
91
|
+
}).format(date);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const tooltipContent = (
|
|
95
|
+
<div className="text-center max-w-48">
|
|
96
|
+
<div className="font-semibold">{badge.name}</div>
|
|
97
|
+
<div className="text-ink-300 mt-1">{badge.description}</div>
|
|
98
|
+
{variant === 'earned' && badge.earnedAt && (
|
|
99
|
+
<div className="text-success-400 mt-2 text-2xs">
|
|
100
|
+
Earned {formatDate(badge.earnedAt)}
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
{variant === 'in-progress' && (
|
|
104
|
+
<div className="text-accent-400 mt-2 text-2xs">
|
|
105
|
+
{clampedProgress}% complete
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
{variant === 'locked' && (
|
|
109
|
+
<div className="text-ink-400 mt-2 text-2xs">
|
|
110
|
+
Not yet earned
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const badgeElement = (
|
|
117
|
+
<div
|
|
118
|
+
className={`
|
|
119
|
+
relative
|
|
120
|
+
${styles.container}
|
|
121
|
+
flex items-center justify-center
|
|
122
|
+
${className}
|
|
123
|
+
`}
|
|
124
|
+
role="img"
|
|
125
|
+
aria-label={`${badge.name}: ${variant === 'earned' ? 'Earned' : variant === 'locked' ? 'Locked' : `${clampedProgress}% complete`}`}
|
|
126
|
+
>
|
|
127
|
+
{/* Progress ring for in-progress variant */}
|
|
128
|
+
{variant === 'in-progress' && (
|
|
129
|
+
<svg
|
|
130
|
+
className="absolute inset-0 w-full h-full -rotate-90"
|
|
131
|
+
viewBox={`0 0 ${ringSize} ${ringSize}`}
|
|
132
|
+
>
|
|
133
|
+
{/* Background ring */}
|
|
134
|
+
<circle
|
|
135
|
+
cx={ringSize / 2}
|
|
136
|
+
cy={ringSize / 2}
|
|
137
|
+
r={ringRadius}
|
|
138
|
+
fill="none"
|
|
139
|
+
stroke="currentColor"
|
|
140
|
+
strokeWidth={styles.ringStroke}
|
|
141
|
+
className="text-paper-200"
|
|
142
|
+
/>
|
|
143
|
+
{/* Progress ring */}
|
|
144
|
+
<circle
|
|
145
|
+
cx={ringSize / 2}
|
|
146
|
+
cy={ringSize / 2}
|
|
147
|
+
r={ringRadius}
|
|
148
|
+
fill="none"
|
|
149
|
+
stroke="currentColor"
|
|
150
|
+
strokeWidth={styles.ringStroke}
|
|
151
|
+
strokeLinecap="round"
|
|
152
|
+
strokeDasharray={ringCircumference}
|
|
153
|
+
strokeDashoffset={ringOffset}
|
|
154
|
+
className="text-accent-500 transition-all duration-500"
|
|
155
|
+
/>
|
|
156
|
+
</svg>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{/* Badge icon container */}
|
|
160
|
+
<div
|
|
161
|
+
className={`
|
|
162
|
+
${styles.iconContainer}
|
|
163
|
+
rounded-full
|
|
164
|
+
flex items-center justify-center
|
|
165
|
+
${variantStyles[variant]}
|
|
166
|
+
${variant === 'earned' ? 'animate-scale-in' : ''}
|
|
167
|
+
transition-all duration-300
|
|
168
|
+
`}
|
|
169
|
+
>
|
|
170
|
+
<div className={styles.icon}>
|
|
171
|
+
{badge.icon}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{/* Glow effect for earned badges */}
|
|
176
|
+
{variant === 'earned' && (
|
|
177
|
+
<div
|
|
178
|
+
className="absolute inset-0 rounded-full bg-success-400/20 animate-pulse-slow -z-10 blur-md"
|
|
179
|
+
style={{ transform: 'scale(1.1)' }}
|
|
180
|
+
/>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (showTooltip) {
|
|
186
|
+
return (
|
|
187
|
+
<Tooltip content={tooltipContent} position="top">
|
|
188
|
+
{badgeElement}
|
|
189
|
+
</Tooltip>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return badgeElement;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export default AchievementBadge;
|