@lssm/example.learning-journey-ui-shared 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.
- package/.turbo/turbo-build.log +24 -0
- package/CHANGELOG.md +10 -0
- package/README.md +39 -0
- package/dist/components/index.d.mts +2 -0
- package/dist/components/index.mjs +3 -0
- package/dist/components-tyJAN4Ru.mjs +164 -0
- package/dist/hooks/index.d.mts +2 -0
- package/dist/hooks/index.mjs +3 -0
- package/dist/hooks-B-tDvppY.mjs +71 -0
- package/dist/index-D_7WU_xm.d.mts +21 -0
- package/dist/index-EWErSKip.d.mts +34 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +4 -0
- package/dist/types-BMAby_Ku.d.mts +57 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +1 -0
- package/package.json +52 -0
- package/src/components/BadgeDisplay.tsx +66 -0
- package/src/components/StreakCounter.tsx +50 -0
- package/src/components/ViewTabs.tsx +47 -0
- package/src/components/XpBar.tsx +55 -0
- package/src/components/index.ts +5 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/useLearningProgress.ts +102 -0
- package/src/index.ts +18 -0
- package/src/types.ts +62 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.js +14 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Progress } from '@lssm/lib.ui-kit-web/ui/progress';
|
|
4
|
+
import { cn } from '@lssm/lib.ui-kit-web/ui/utils';
|
|
5
|
+
import type { XpBarProps } from '../types';
|
|
6
|
+
|
|
7
|
+
const sizeStyles = {
|
|
8
|
+
sm: 'h-2',
|
|
9
|
+
md: 'h-3',
|
|
10
|
+
lg: 'h-4',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const labelSizeStyles = {
|
|
14
|
+
sm: 'text-xs',
|
|
15
|
+
md: 'text-sm',
|
|
16
|
+
lg: 'text-base',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function XpBar({
|
|
20
|
+
current,
|
|
21
|
+
max,
|
|
22
|
+
level,
|
|
23
|
+
showLabel = true,
|
|
24
|
+
size = 'md',
|
|
25
|
+
}: XpBarProps) {
|
|
26
|
+
const percentage = max > 0 ? Math.min((current / max) * 100, 100) : 0;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="w-full space-y-1">
|
|
30
|
+
{showLabel && (
|
|
31
|
+
<div
|
|
32
|
+
className={cn(
|
|
33
|
+
'flex items-center justify-between',
|
|
34
|
+
labelSizeStyles[size]
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
<span className="text-muted-foreground font-medium">
|
|
38
|
+
{level !== undefined && (
|
|
39
|
+
<span className="text-primary mr-1">Lvl {level}</span>
|
|
40
|
+
)}
|
|
41
|
+
XP
|
|
42
|
+
</span>
|
|
43
|
+
<span className="font-semibold">
|
|
44
|
+
{current.toLocaleString()} / {max.toLocaleString()}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
<Progress
|
|
49
|
+
value={percentage}
|
|
50
|
+
className={cn('bg-muted', sizeStyles[size])}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
4
|
+
import type { LearningJourneyTrackSpec } from '@lssm/module.learning-journey/track-spec';
|
|
5
|
+
import type { LearningProgressState } from '../types';
|
|
6
|
+
|
|
7
|
+
/** Default progress state for a new track */
|
|
8
|
+
function createDefaultProgress(trackId: string): LearningProgressState {
|
|
9
|
+
return {
|
|
10
|
+
trackId,
|
|
11
|
+
completedStepIds: [],
|
|
12
|
+
currentStepId: null,
|
|
13
|
+
xpEarned: 0,
|
|
14
|
+
streakDays: 0,
|
|
15
|
+
lastActivityDate: null,
|
|
16
|
+
badges: [],
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Hook for managing learning progress state */
|
|
21
|
+
export function useLearningProgress(track: LearningJourneyTrackSpec) {
|
|
22
|
+
const [progress, setProgress] = useState<LearningProgressState>(() =>
|
|
23
|
+
createDefaultProgress(track.id)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const completeStep = useCallback(
|
|
27
|
+
(stepId: string) => {
|
|
28
|
+
const step = track.steps.find((s) => s.id === stepId);
|
|
29
|
+
if (!step || progress.completedStepIds.includes(stepId)) return;
|
|
30
|
+
|
|
31
|
+
setProgress((prev) => {
|
|
32
|
+
const newCompletedIds = [...prev.completedStepIds, stepId];
|
|
33
|
+
const xpReward = step.xpReward ?? 0;
|
|
34
|
+
|
|
35
|
+
// Find next incomplete step
|
|
36
|
+
const nextStep = track.steps.find(
|
|
37
|
+
(s) => !newCompletedIds.includes(s.id)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Check if track is complete
|
|
41
|
+
const isTrackComplete = newCompletedIds.length === track.steps.length;
|
|
42
|
+
const completionBonus = isTrackComplete
|
|
43
|
+
? (track.completionRewards?.xpBonus ?? 0)
|
|
44
|
+
: 0;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
...prev,
|
|
48
|
+
completedStepIds: newCompletedIds,
|
|
49
|
+
currentStepId: nextStep?.id ?? null,
|
|
50
|
+
xpEarned: prev.xpEarned + xpReward + completionBonus,
|
|
51
|
+
lastActivityDate: new Date().toISOString(),
|
|
52
|
+
badges:
|
|
53
|
+
isTrackComplete && track.completionRewards?.badgeKey
|
|
54
|
+
? [...prev.badges, track.completionRewards.badgeKey]
|
|
55
|
+
: prev.badges,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
[track, progress.completedStepIds]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const resetProgress = useCallback(() => {
|
|
63
|
+
setProgress(createDefaultProgress(track.id));
|
|
64
|
+
}, [track.id]);
|
|
65
|
+
|
|
66
|
+
const incrementStreak = useCallback(() => {
|
|
67
|
+
setProgress((prev) => ({
|
|
68
|
+
...prev,
|
|
69
|
+
streakDays: prev.streakDays + 1,
|
|
70
|
+
lastActivityDate: new Date().toISOString(),
|
|
71
|
+
}));
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const stats = useMemo(() => {
|
|
75
|
+
const totalSteps = track.steps.length;
|
|
76
|
+
const completedSteps = progress.completedStepIds.length;
|
|
77
|
+
const percentComplete =
|
|
78
|
+
totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
|
|
79
|
+
const totalXp =
|
|
80
|
+
track.totalXp ??
|
|
81
|
+
track.steps.reduce((sum, s) => sum + (s.xpReward ?? 0), 0) +
|
|
82
|
+
(track.completionRewards?.xpBonus ?? 0);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
totalSteps,
|
|
86
|
+
completedSteps,
|
|
87
|
+
remainingSteps: totalSteps - completedSteps,
|
|
88
|
+
percentComplete,
|
|
89
|
+
totalXp,
|
|
90
|
+
isComplete: completedSteps === totalSteps,
|
|
91
|
+
};
|
|
92
|
+
}, [track, progress.completedStepIds]);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
progress,
|
|
96
|
+
stats,
|
|
97
|
+
completeStep,
|
|
98
|
+
resetProgress,
|
|
99
|
+
incrementStreak,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Hooks
|
|
2
|
+
export { useLearningProgress } from './hooks';
|
|
3
|
+
|
|
4
|
+
// Components
|
|
5
|
+
export { XpBar, StreakCounter, BadgeDisplay, ViewTabs } from './components';
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
export type {
|
|
9
|
+
LearningView,
|
|
10
|
+
LearningProgressState,
|
|
11
|
+
LearningMiniAppProps,
|
|
12
|
+
LearningViewProps,
|
|
13
|
+
XpBarProps,
|
|
14
|
+
StreakCounterProps,
|
|
15
|
+
BadgeDisplayProps,
|
|
16
|
+
ViewTabsProps,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { LearningJourneyTrackSpec } from '@lssm/module.learning-journey/track-spec';
|
|
2
|
+
|
|
3
|
+
/** View types for learning mini-apps */
|
|
4
|
+
export type LearningView = 'overview' | 'steps' | 'progress' | 'timeline';
|
|
5
|
+
|
|
6
|
+
/** Progress state for a learning track */
|
|
7
|
+
export interface LearningProgressState {
|
|
8
|
+
trackId: string;
|
|
9
|
+
completedStepIds: string[];
|
|
10
|
+
currentStepId: string | null;
|
|
11
|
+
xpEarned: number;
|
|
12
|
+
streakDays: number;
|
|
13
|
+
lastActivityDate: string | null;
|
|
14
|
+
badges: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Props for mini-app components */
|
|
18
|
+
export interface LearningMiniAppProps {
|
|
19
|
+
track: LearningJourneyTrackSpec;
|
|
20
|
+
progress: LearningProgressState;
|
|
21
|
+
onStepComplete?: (stepId: string) => void;
|
|
22
|
+
onViewChange?: (view: LearningView) => void;
|
|
23
|
+
initialView?: LearningView;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Props for view components */
|
|
27
|
+
export interface LearningViewProps {
|
|
28
|
+
track: LearningJourneyTrackSpec;
|
|
29
|
+
progress: LearningProgressState;
|
|
30
|
+
onStepComplete?: (stepId: string) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** XP bar props */
|
|
34
|
+
export interface XpBarProps {
|
|
35
|
+
current: number;
|
|
36
|
+
max: number;
|
|
37
|
+
level?: number;
|
|
38
|
+
showLabel?: boolean;
|
|
39
|
+
size?: 'sm' | 'md' | 'lg';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Streak counter props */
|
|
43
|
+
export interface StreakCounterProps {
|
|
44
|
+
days: number;
|
|
45
|
+
isActive?: boolean;
|
|
46
|
+
size?: 'sm' | 'md' | 'lg';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Badge display props */
|
|
50
|
+
export interface BadgeDisplayProps {
|
|
51
|
+
badges: string[];
|
|
52
|
+
maxVisible?: number;
|
|
53
|
+
size?: 'sm' | 'md' | 'lg';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** View tabs props */
|
|
57
|
+
export interface ViewTabsProps {
|
|
58
|
+
currentView: LearningView;
|
|
59
|
+
onViewChange: (view: LearningView) => void;
|
|
60
|
+
availableViews?: LearningView[];
|
|
61
|
+
}
|
|
62
|
+
|