@lssm/example.learning-journey-ui-coaching 0.0.0-canary-20251212210835

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.
@@ -0,0 +1,421 @@
1
+ import { n as EngagementMeter, r as TipCard, t as TipFeed } from "./TipFeed-_1oKR35X.mjs";
2
+ import { Card, CardContent, CardHeader, CardTitle } from "@lssm/lib.ui-kit-web/ui/card";
3
+ import { BadgeDisplay, StreakCounter, XpBar } from "@lssm/example.learning-journey-ui-shared";
4
+ import { Button } from "@lssm/lib.design-system";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+
7
+ //#region src/views/Overview.tsx
8
+ function Overview({ track, progress, onStepComplete, onStart }) {
9
+ const totalXp = track.totalXp ?? track.steps.reduce((sum, s) => sum + (s.xpReward ?? 0), 0);
10
+ const completedSteps = progress.completedStepIds.length;
11
+ const totalSteps = track.steps.length;
12
+ const pendingSteps = totalSteps - completedSteps;
13
+ const activeTips = track.steps.filter((s) => !progress.completedStepIds.includes(s.id)).slice(0, 3);
14
+ return /* @__PURE__ */ jsxs("div", {
15
+ className: "space-y-6",
16
+ children: [
17
+ /* @__PURE__ */ jsx(Card, {
18
+ className: "overflow-hidden bg-gradient-to-br from-amber-500/10 via-orange-500/10 to-red-500/10",
19
+ children: /* @__PURE__ */ jsx(CardContent, {
20
+ className: "p-6",
21
+ children: /* @__PURE__ */ jsxs("div", {
22
+ className: "flex flex-col items-center gap-4 text-center md:flex-row md:text-left",
23
+ children: [
24
+ /* @__PURE__ */ jsx("div", {
25
+ className: "flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-amber-500 to-orange-600 text-3xl shadow-lg",
26
+ children: "💡"
27
+ }),
28
+ /* @__PURE__ */ jsxs("div", {
29
+ className: "flex-1",
30
+ children: [/* @__PURE__ */ jsx("h1", {
31
+ className: "text-2xl font-bold",
32
+ children: track.name
33
+ }), /* @__PURE__ */ jsx("p", {
34
+ className: "text-muted-foreground mt-1",
35
+ children: track.description
36
+ })]
37
+ }),
38
+ /* @__PURE__ */ jsx("div", {
39
+ className: "flex items-center gap-4",
40
+ children: /* @__PURE__ */ jsx(StreakCounter, {
41
+ days: progress.streakDays,
42
+ size: "lg"
43
+ })
44
+ })
45
+ ]
46
+ })
47
+ })
48
+ }),
49
+ /* @__PURE__ */ jsxs("div", {
50
+ className: "grid gap-4 md:grid-cols-3",
51
+ children: [
52
+ /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(CardHeader, {
53
+ className: "pb-2",
54
+ children: /* @__PURE__ */ jsx(CardTitle, {
55
+ className: "text-muted-foreground text-sm font-medium",
56
+ children: "Active Tips"
57
+ })
58
+ }), /* @__PURE__ */ jsxs(CardContent, { children: [/* @__PURE__ */ jsx("div", {
59
+ className: "text-3xl font-bold text-amber-500",
60
+ children: pendingSteps
61
+ }), /* @__PURE__ */ jsx("p", {
62
+ className: "text-muted-foreground text-sm",
63
+ children: "tips for you today"
64
+ })] })] }),
65
+ /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(CardHeader, {
66
+ className: "pb-2",
67
+ children: /* @__PURE__ */ jsx(CardTitle, {
68
+ className: "text-muted-foreground text-sm font-medium",
69
+ children: "Tips Actioned"
70
+ })
71
+ }), /* @__PURE__ */ jsxs(CardContent, { children: [/* @__PURE__ */ jsx("div", {
72
+ className: "text-3xl font-bold text-green-500",
73
+ children: completedSteps
74
+ }), /* @__PURE__ */ jsxs("p", {
75
+ className: "text-muted-foreground text-sm",
76
+ children: [
77
+ "out of ",
78
+ totalSteps,
79
+ " total"
80
+ ]
81
+ })] })] }),
82
+ /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(CardHeader, {
83
+ className: "pb-2",
84
+ children: /* @__PURE__ */ jsx(CardTitle, {
85
+ className: "text-muted-foreground text-sm font-medium",
86
+ children: "XP Earned"
87
+ })
88
+ }), /* @__PURE__ */ jsxs(CardContent, { children: [/* @__PURE__ */ jsx("div", {
89
+ className: "text-3xl font-bold text-orange-500",
90
+ children: progress.xpEarned
91
+ }), /* @__PURE__ */ jsx(XpBar, {
92
+ current: progress.xpEarned,
93
+ max: totalXp,
94
+ showLabel: false,
95
+ size: "sm"
96
+ })] })] })
97
+ ]
98
+ }),
99
+ activeTips.length > 0 && /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsxs(CardHeader, {
100
+ className: "flex flex-row items-center justify-between",
101
+ children: [/* @__PURE__ */ jsxs(CardTitle, {
102
+ className: "flex items-center gap-2",
103
+ children: [/* @__PURE__ */ jsx("span", { children: "💡" }), /* @__PURE__ */ jsx("span", { children: "Tips for You" })]
104
+ }), activeTips.length < pendingSteps && /* @__PURE__ */ jsxs(Button, {
105
+ variant: "outline",
106
+ size: "sm",
107
+ onClick: onStart,
108
+ children: [
109
+ "View All (",
110
+ pendingSteps,
111
+ ")"
112
+ ]
113
+ })]
114
+ }), /* @__PURE__ */ jsx(CardContent, {
115
+ className: "space-y-3",
116
+ children: activeTips.map((step) => /* @__PURE__ */ jsx(TipCard, {
117
+ step,
118
+ isCompleted: false,
119
+ isCurrent: step.id === activeTips[0]?.id,
120
+ onComplete: () => onStepComplete?.(step.id)
121
+ }, step.id))
122
+ })] }),
123
+ pendingSteps === 0 && /* @__PURE__ */ jsx(Card, {
124
+ className: "border-green-500/50 bg-green-500/5",
125
+ children: /* @__PURE__ */ jsxs(CardContent, {
126
+ className: "flex items-center gap-4 p-6",
127
+ children: [/* @__PURE__ */ jsx("div", {
128
+ className: "text-4xl",
129
+ children: "🎉"
130
+ }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
131
+ className: "text-lg font-semibold text-green-500",
132
+ children: "All Tips Actioned!"
133
+ }), /* @__PURE__ */ jsxs("p", {
134
+ className: "text-muted-foreground",
135
+ children: [
136
+ "Great job! You've addressed all ",
137
+ totalSteps,
138
+ " coaching tips."
139
+ ]
140
+ })] })]
141
+ })
142
+ })
143
+ ]
144
+ });
145
+ }
146
+
147
+ //#endregion
148
+ //#region src/views/Steps.tsx
149
+ function Steps({ track, progress, onStepComplete }) {
150
+ const completedSteps = progress.completedStepIds.length;
151
+ const totalSteps = track.steps.length;
152
+ const sortedSteps = [...track.steps].sort((a, b) => {
153
+ const aCompleted = progress.completedStepIds.includes(a.id);
154
+ if (aCompleted === progress.completedStepIds.includes(b.id)) return 0;
155
+ return aCompleted ? 1 : -1;
156
+ });
157
+ const currentStepId = track.steps.find((s) => !progress.completedStepIds.includes(s.id))?.id;
158
+ return /* @__PURE__ */ jsxs("div", {
159
+ className: "space-y-6",
160
+ children: [
161
+ /* @__PURE__ */ jsxs("div", {
162
+ className: "text-center",
163
+ children: [
164
+ /* @__PURE__ */ jsx("h2", {
165
+ className: "text-xl font-bold",
166
+ children: "Coaching Tips"
167
+ }),
168
+ /* @__PURE__ */ jsx("p", {
169
+ className: "text-muted-foreground",
170
+ children: "Review and take action on personalized tips"
171
+ }),
172
+ /* @__PURE__ */ jsxs("p", {
173
+ className: "text-muted-foreground mt-2 text-sm",
174
+ children: [
175
+ completedSteps,
176
+ " of ",
177
+ totalSteps,
178
+ " tips actioned"
179
+ ]
180
+ })
181
+ ]
182
+ }),
183
+ /* @__PURE__ */ jsx("div", {
184
+ className: "space-y-3",
185
+ children: sortedSteps.map((step) => {
186
+ return /* @__PURE__ */ jsx(TipCard, {
187
+ step,
188
+ isCompleted: progress.completedStepIds.includes(step.id),
189
+ isCurrent: step.id === currentStepId,
190
+ onComplete: () => onStepComplete?.(step.id),
191
+ onDismiss: () => onStepComplete?.(step.id)
192
+ }, step.id);
193
+ })
194
+ }),
195
+ completedSteps === totalSteps && /* @__PURE__ */ jsxs("div", {
196
+ className: "text-muted-foreground text-center",
197
+ children: [/* @__PURE__ */ jsx("span", {
198
+ className: "text-2xl",
199
+ children: "✨"
200
+ }), /* @__PURE__ */ jsx("p", {
201
+ className: "mt-2",
202
+ children: "All tips have been addressed!"
203
+ })]
204
+ })
205
+ ]
206
+ });
207
+ }
208
+
209
+ //#endregion
210
+ //#region src/views/Progress.tsx
211
+ function ProgressView({ track, progress }) {
212
+ const totalXp = track.totalXp ?? track.steps.reduce((sum, s) => sum + (s.xpReward ?? 0), 0);
213
+ const completedSteps = progress.completedStepIds.length;
214
+ const pendingSteps = track.steps.length - completedSteps;
215
+ const actioned = Math.floor(completedSteps * .7);
216
+ const acknowledged = completedSteps - actioned;
217
+ return /* @__PURE__ */ jsxs("div", {
218
+ className: "space-y-6",
219
+ children: [
220
+ /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, {
221
+ className: "flex items-center gap-2",
222
+ children: [/* @__PURE__ */ jsx("span", { children: "📊" }), /* @__PURE__ */ jsx("span", { children: "Engagement Overview" })]
223
+ }) }), /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx(EngagementMeter, {
224
+ actioned,
225
+ acknowledged,
226
+ pending: pendingSteps,
227
+ streak: progress.streakDays
228
+ }) })] }),
229
+ /* @__PURE__ */ jsxs("div", {
230
+ className: "grid gap-4 md:grid-cols-2",
231
+ children: [/* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(CardHeader, {
232
+ className: "pb-2",
233
+ children: /* @__PURE__ */ jsx(CardTitle, {
234
+ className: "text-muted-foreground text-sm font-medium",
235
+ children: "XP Earned"
236
+ })
237
+ }), /* @__PURE__ */ jsxs(CardContent, {
238
+ className: "space-y-3",
239
+ children: [/* @__PURE__ */ jsxs("div", {
240
+ className: "flex items-baseline gap-2",
241
+ children: [/* @__PURE__ */ jsx("span", {
242
+ className: "text-3xl font-bold text-orange-500",
243
+ children: progress.xpEarned
244
+ }), /* @__PURE__ */ jsxs("span", {
245
+ className: "text-muted-foreground",
246
+ children: [
247
+ "/ ",
248
+ totalXp,
249
+ " XP"
250
+ ]
251
+ })]
252
+ }), /* @__PURE__ */ jsx(XpBar, {
253
+ current: progress.xpEarned,
254
+ max: totalXp,
255
+ showLabel: false,
256
+ size: "lg"
257
+ })]
258
+ })] }), /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(CardHeader, {
259
+ className: "pb-2",
260
+ children: /* @__PURE__ */ jsx(CardTitle, {
261
+ className: "text-muted-foreground text-sm font-medium",
262
+ children: "Engagement Streak"
263
+ })
264
+ }), /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs("div", {
265
+ className: "flex items-center gap-4",
266
+ children: [/* @__PURE__ */ jsx(StreakCounter, {
267
+ days: progress.streakDays,
268
+ size: "lg"
269
+ }), /* @__PURE__ */ jsx("div", {
270
+ className: "text-muted-foreground text-sm",
271
+ children: progress.streakDays > 0 ? "Keep going!" : "Start your streak today!"
272
+ })]
273
+ }) })] })]
274
+ }),
275
+ /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, {
276
+ className: "flex items-center gap-2",
277
+ children: [/* @__PURE__ */ jsx("span", { children: "🏅" }), /* @__PURE__ */ jsx("span", { children: "Achievements" })]
278
+ }) }), /* @__PURE__ */ jsxs(CardContent, { children: [/* @__PURE__ */ jsx(BadgeDisplay, {
279
+ badges: progress.badges,
280
+ size: "lg",
281
+ maxVisible: 10
282
+ }), progress.badges.length === 0 && /* @__PURE__ */ jsx("p", {
283
+ className: "text-muted-foreground text-sm",
284
+ children: "Action tips to unlock achievements!"
285
+ })] })] }),
286
+ /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, {
287
+ className: "flex items-center gap-2",
288
+ children: [/* @__PURE__ */ jsx("span", { children: "💡" }), /* @__PURE__ */ jsx("span", { children: "Tip Status" })]
289
+ }) }), /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx("div", {
290
+ className: "space-y-3",
291
+ children: track.steps.map((step) => {
292
+ const isCompleted = progress.completedStepIds.includes(step.id);
293
+ return /* @__PURE__ */ jsxs("div", {
294
+ className: "flex items-center justify-between rounded-lg border p-3",
295
+ children: [/* @__PURE__ */ jsxs("div", {
296
+ className: "flex items-center gap-3",
297
+ children: [/* @__PURE__ */ jsx("span", {
298
+ className: isCompleted ? "text-green-500" : "text-amber-500",
299
+ children: isCompleted ? "✓" : "○"
300
+ }), /* @__PURE__ */ jsx("span", {
301
+ className: isCompleted ? "text-muted-foreground" : "text-foreground",
302
+ children: step.title
303
+ })]
304
+ }), /* @__PURE__ */ jsx("span", {
305
+ className: `text-sm ${isCompleted ? "text-green-500" : "text-muted-foreground"}`,
306
+ children: isCompleted ? "Actioned" : "Pending"
307
+ })]
308
+ }, step.id);
309
+ })
310
+ }) })] })
311
+ ]
312
+ });
313
+ }
314
+
315
+ //#endregion
316
+ //#region src/views/Timeline.tsx
317
+ function Timeline({ track, progress }) {
318
+ const feedItems = track.steps.map((step) => ({
319
+ step,
320
+ isCompleted: progress.completedStepIds.includes(step.id),
321
+ completedAt: progress.completedStepIds.includes(step.id) ? "Recently" : void 0
322
+ })).sort((a, b) => {
323
+ if (a.isCompleted && !b.isCompleted) return -1;
324
+ if (!a.isCompleted && b.isCompleted) return 1;
325
+ return 0;
326
+ });
327
+ const completedCount = feedItems.filter((f) => f.isCompleted).length;
328
+ const pendingCount = feedItems.length - completedCount;
329
+ return /* @__PURE__ */ jsxs("div", {
330
+ className: "space-y-6",
331
+ children: [
332
+ /* @__PURE__ */ jsxs("div", {
333
+ className: "text-center",
334
+ children: [/* @__PURE__ */ jsx("h2", {
335
+ className: "text-xl font-bold",
336
+ children: "Activity Timeline"
337
+ }), /* @__PURE__ */ jsx("p", {
338
+ className: "text-muted-foreground",
339
+ children: "Your coaching journey and tip history"
340
+ })]
341
+ }),
342
+ /* @__PURE__ */ jsxs("div", {
343
+ className: "grid grid-cols-2 gap-4",
344
+ children: [/* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, {
345
+ className: "p-4 text-center",
346
+ children: [/* @__PURE__ */ jsx("div", {
347
+ className: "text-2xl font-bold text-green-500",
348
+ children: completedCount
349
+ }), /* @__PURE__ */ jsx("div", {
350
+ className: "text-muted-foreground text-sm",
351
+ children: "Tips Actioned"
352
+ })]
353
+ }) }), /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, {
354
+ className: "p-4 text-center",
355
+ children: [/* @__PURE__ */ jsx("div", {
356
+ className: "text-2xl font-bold text-amber-500",
357
+ children: pendingCount
358
+ }), /* @__PURE__ */ jsx("div", {
359
+ className: "text-muted-foreground text-sm",
360
+ children: "Tips Pending"
361
+ })]
362
+ }) })]
363
+ }),
364
+ /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, {
365
+ className: "flex items-center gap-2",
366
+ children: [/* @__PURE__ */ jsx("span", { children: "📝" }), /* @__PURE__ */ jsx("span", { children: "Coaching Feed" })]
367
+ }) }), /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx(TipFeed, { items: feedItems }) })] }),
368
+ /* @__PURE__ */ jsxs(Card, { children: [/* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsxs(CardTitle, {
369
+ className: "flex items-center gap-2",
370
+ children: [/* @__PURE__ */ jsx("span", { children: "📈" }), /* @__PURE__ */ jsx("span", { children: "Journey Stats" })]
371
+ }) }), /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs("div", {
372
+ className: "space-y-4",
373
+ children: [
374
+ /* @__PURE__ */ jsxs("div", {
375
+ className: "flex items-center justify-between",
376
+ children: [/* @__PURE__ */ jsx("span", {
377
+ className: "text-muted-foreground",
378
+ children: "Total Tips"
379
+ }), /* @__PURE__ */ jsx("span", {
380
+ className: "font-semibold",
381
+ children: track.steps.length
382
+ })]
383
+ }),
384
+ /* @__PURE__ */ jsxs("div", {
385
+ className: "flex items-center justify-between",
386
+ children: [/* @__PURE__ */ jsx("span", {
387
+ className: "text-muted-foreground",
388
+ children: "Completed"
389
+ }), /* @__PURE__ */ jsx("span", {
390
+ className: "font-semibold text-green-500",
391
+ children: completedCount
392
+ })]
393
+ }),
394
+ /* @__PURE__ */ jsxs("div", {
395
+ className: "flex items-center justify-between",
396
+ children: [/* @__PURE__ */ jsx("span", {
397
+ className: "text-muted-foreground",
398
+ children: "XP Earned"
399
+ }), /* @__PURE__ */ jsx("span", {
400
+ className: "font-semibold text-orange-500",
401
+ children: progress.xpEarned
402
+ })]
403
+ }),
404
+ /* @__PURE__ */ jsxs("div", {
405
+ className: "flex items-center justify-between",
406
+ children: [/* @__PURE__ */ jsx("span", {
407
+ className: "text-muted-foreground",
408
+ children: "Current Streak"
409
+ }), /* @__PURE__ */ jsx("span", {
410
+ className: "font-semibold",
411
+ children: progress.streakDays > 0 ? `🔥 ${progress.streakDays} days` : "Start today!"
412
+ })]
413
+ })
414
+ ]
415
+ }) })] })
416
+ ]
417
+ });
418
+ }
419
+
420
+ //#endregion
421
+ export { Overview as i, ProgressView as n, Steps as r, Timeline as t };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@lssm/example.learning-journey-ui-coaching",
3
+ "version": "0.0.0-canary-20251212210835",
4
+ "description": "Contextual coaching UI with tip cards and engagement tracking.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./views": "./src/views/index.ts",
11
+ "./components": "./src/components/index.ts",
12
+ "./*": "./*"
13
+ },
14
+ "scripts": {
15
+ "build": "bun build:bundle && bun build:types",
16
+ "build:bundle": "tsdown",
17
+ "build:types": "tsc --noEmit",
18
+ "dev": "bun build:bundle --watch",
19
+ "clean": "rimraf dist .turbo",
20
+ "lint": "bun lint:fix",
21
+ "lint:fix": "eslint src --fix",
22
+ "lint:check": "eslint src",
23
+ "test": "bun test"
24
+ },
25
+ "dependencies": {
26
+ "@lssm/example.learning-journey-ui-shared": "workspace:*",
27
+ "@lssm/example.learning-journey.ambient-coach": "workspace:*",
28
+ "@lssm/example.learning-journey.crm-onboarding": "workspace:*",
29
+ "@lssm/module.learning-journey": "workspace:*",
30
+ "@lssm/lib.design-system": "workspace:*",
31
+ "@lssm/lib.ui-kit-web": "workspace:*",
32
+ "react": "^19.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "@lssm/tool.tsdown": "workspace:*",
36
+ "@lssm/tool.typescript": "workspace:*",
37
+ "@types/react": "^19.1.6",
38
+ "tsdown": "^0.17.0",
39
+ "typescript": "^5.9.3"
40
+ },
41
+ "peerDependencies": {
42
+ "react": "^18.0.0 || ^19.0.0"
43
+ },
44
+ "module": "./dist/index.js",
45
+ "publishConfig": {
46
+ "exports": {
47
+ ".": "./dist/index.js",
48
+ "./views": "./dist/views/index.js",
49
+ "./components": "./dist/components/index.js",
50
+ "./*": "./*"
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+ import { Card, CardContent } from '@lssm/lib.ui-kit-web/ui/card';
5
+ import {
6
+ ViewTabs,
7
+ useLearningProgress,
8
+ type LearningView,
9
+ type LearningMiniAppProps,
10
+ } from '@lssm/example.learning-journey-ui-shared';
11
+ import { Overview } from './views/Overview';
12
+ import { Steps } from './views/Steps';
13
+ import { Progress } from './views/Progress';
14
+ import { Timeline } from './views/Timeline';
15
+
16
+ type CoachingMiniAppProps = Omit<LearningMiniAppProps, 'progress'> & {
17
+ progress?: LearningMiniAppProps['progress'];
18
+ };
19
+
20
+ export function CoachingMiniApp({
21
+ track,
22
+ progress: externalProgress,
23
+ onStepComplete: externalOnStepComplete,
24
+ onViewChange,
25
+ initialView = 'overview',
26
+ }: CoachingMiniAppProps) {
27
+ const [currentView, setCurrentView] = useState<LearningView>(initialView);
28
+
29
+ // Use internal progress if not provided externally
30
+ const { progress: internalProgress, completeStep: internalCompleteStep } =
31
+ useLearningProgress(track);
32
+
33
+ const progress = externalProgress ?? internalProgress;
34
+
35
+ const handleViewChange = useCallback(
36
+ (view: LearningView) => {
37
+ setCurrentView(view);
38
+ onViewChange?.(view);
39
+ },
40
+ [onViewChange]
41
+ );
42
+
43
+ const handleStepComplete = useCallback(
44
+ (stepId: string) => {
45
+ if (externalOnStepComplete) {
46
+ externalOnStepComplete(stepId);
47
+ } else {
48
+ internalCompleteStep(stepId);
49
+ }
50
+ },
51
+ [externalOnStepComplete, internalCompleteStep]
52
+ );
53
+
54
+ const handleStartFromOverview = useCallback(() => {
55
+ setCurrentView('steps');
56
+ onViewChange?.('steps');
57
+ }, [onViewChange]);
58
+
59
+ const renderView = () => {
60
+ const viewProps = {
61
+ track,
62
+ progress,
63
+ onStepComplete: handleStepComplete,
64
+ };
65
+
66
+ switch (currentView) {
67
+ case 'overview':
68
+ return <Overview {...viewProps} onStart={handleStartFromOverview} />;
69
+ case 'steps':
70
+ return <Steps {...viewProps} />;
71
+ case 'progress':
72
+ return <Progress {...viewProps} />;
73
+ case 'timeline':
74
+ return <Timeline {...viewProps} />;
75
+ default:
76
+ return <Overview {...viewProps} onStart={handleStartFromOverview} />;
77
+ }
78
+ };
79
+
80
+ return (
81
+ <div className="space-y-6">
82
+ {/* Navigation */}
83
+ <Card>
84
+ <CardContent className="p-4">
85
+ <ViewTabs currentView={currentView} onViewChange={handleViewChange} />
86
+ </CardContent>
87
+ </Card>
88
+
89
+ {/* Current View */}
90
+ {renderView()}
91
+ </div>
92
+ );
93
+ }
94
+
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@lssm/lib.ui-kit-web/ui/utils';
4
+
5
+ interface EngagementMeterProps {
6
+ acknowledged: number;
7
+ actioned: number;
8
+ pending: number;
9
+ streak?: number;
10
+ }
11
+
12
+ export function EngagementMeter({
13
+ acknowledged,
14
+ actioned,
15
+ pending,
16
+ streak = 0,
17
+ }: EngagementMeterProps) {
18
+ const total = acknowledged + actioned + pending;
19
+ const actionedPercent = total > 0 ? (actioned / total) * 100 : 0;
20
+ const acknowledgedPercent = total > 0 ? (acknowledged / total) * 100 : 0;
21
+
22
+ return (
23
+ <div className="space-y-4">
24
+ {/* Donut chart */}
25
+ <div className="flex items-center justify-center">
26
+ <div className="relative h-32 w-32">
27
+ <svg className="h-full w-full -rotate-90" viewBox="0 0 100 100">
28
+ {/* Background */}
29
+ <circle
30
+ cx="50"
31
+ cy="50"
32
+ r="40"
33
+ fill="none"
34
+ strokeWidth="12"
35
+ className="stroke-muted"
36
+ />
37
+ {/* Actioned (green) */}
38
+ <circle
39
+ cx="50"
40
+ cy="50"
41
+ r="40"
42
+ fill="none"
43
+ strokeWidth="12"
44
+ strokeLinecap="round"
45
+ strokeDasharray={`${actionedPercent * 2.51} 251`}
46
+ className="stroke-green-500 transition-all duration-500"
47
+ />
48
+ {/* Acknowledged (amber) - offset by actioned */}
49
+ <circle
50
+ cx="50"
51
+ cy="50"
52
+ r="40"
53
+ fill="none"
54
+ strokeWidth="12"
55
+ strokeLinecap="round"
56
+ strokeDasharray={`${acknowledgedPercent * 2.51} 251`}
57
+ strokeDashoffset={`${-actionedPercent * 2.51}`}
58
+ className="stroke-amber-500 transition-all duration-500"
59
+ />
60
+ </svg>
61
+ <div className="absolute inset-0 flex flex-col items-center justify-center">
62
+ <span className="text-2xl font-bold">{total}</span>
63
+ <span className="text-muted-foreground text-xs">tips</span>
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ {/* Legend */}
69
+ <div className="flex justify-center gap-4 text-sm">
70
+ <div className="flex items-center gap-1.5">
71
+ <div className="h-3 w-3 rounded-full bg-green-500" />
72
+ <span>Actioned ({actioned})</span>
73
+ </div>
74
+ <div className="flex items-center gap-1.5">
75
+ <div className="h-3 w-3 rounded-full bg-amber-500" />
76
+ <span>Acknowledged ({acknowledged})</span>
77
+ </div>
78
+ <div className="flex items-center gap-1.5">
79
+ <div className="bg-muted h-3 w-3 rounded-full" />
80
+ <span>Pending ({pending})</span>
81
+ </div>
82
+ </div>
83
+
84
+ {/* Streak */}
85
+ {streak > 0 && (
86
+ <div className="flex items-center justify-center gap-2 rounded-lg bg-orange-500/10 px-4 py-2">
87
+ <span className="text-xl">🔥</span>
88
+ <span className="font-semibold text-orange-500">
89
+ {streak} day engagement streak!
90
+ </span>
91
+ </div>
92
+ )}
93
+ </div>
94
+ );
95
+ }
96
+