feat: enhance tracking of user interactions in test components by integrating analytics for attempt start, finish, and abandon events. Update logic to handle undefined package properties gracefully and improve loading state handling in the TakeTestComponent.
Browse files
apps/web/src/routes/package.$id.attempt.$attemptId.tsx
CHANGED
|
@@ -43,6 +43,31 @@ function ContinueAttemptComponent() {
|
|
| 43 |
startQuestionTimer,
|
| 44 |
} = useTestSession(packageId, attemptId);
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
if (packageQuery.isLoading) {
|
| 47 |
return (
|
| 48 |
<div className="min-h-screen pt-8 pb-32 px-6 md:px-12 lg:px-16 max-w-4xl mx-auto bg-[var(--warm-cream)]">
|
|
@@ -66,31 +91,6 @@ function ContinueAttemptComponent() {
|
|
| 66 |
);
|
| 67 |
}
|
| 68 |
|
| 69 |
-
const totalQuestions = pkg.sections.reduce((sum: number, sec) => sum + sec.questions.length, 0);
|
| 70 |
-
const answeredCount = Object.keys(answers).length;
|
| 71 |
-
|
| 72 |
-
const handleFinishWithTracking = useCallback(async () => {
|
| 73 |
-
const result = await handleFinish();
|
| 74 |
-
if (result) {
|
| 75 |
-
trackUmamiEvent(AnalyticsEvent.ATTEMPT_FINISH, {
|
| 76 |
-
exam_type: pkg.examTypeName ?? "unknown",
|
| 77 |
-
score: result.totalScore,
|
| 78 |
-
max_score: result.maxScore,
|
| 79 |
-
percentage: result.percentage,
|
| 80 |
-
time_elapsed_sec: timeElapsed,
|
| 81 |
-
});
|
| 82 |
-
}
|
| 83 |
-
}, [handleFinish, pkg.examTypeName, timeElapsed]);
|
| 84 |
-
|
| 85 |
-
const handleAbandonWithTracking = useCallback(async () => {
|
| 86 |
-
await handleAbandon();
|
| 87 |
-
trackUmamiEvent(AnalyticsEvent.ATTEMPT_ABANDON, {
|
| 88 |
-
exam_type: pkg.examTypeName ?? "unknown",
|
| 89 |
-
questions_answered: answeredCount,
|
| 90 |
-
total_questions: totalQuestions,
|
| 91 |
-
});
|
| 92 |
-
}, [handleAbandon, pkg.examTypeName, answeredCount, totalQuestions]);
|
| 93 |
-
|
| 94 |
const currentSection = pkg.sections[currentSectionIdx];
|
| 95 |
if (!currentSection) {
|
| 96 |
return (
|
|
|
|
| 43 |
startQuestionTimer,
|
| 44 |
} = useTestSession(packageId, attemptId);
|
| 45 |
|
| 46 |
+
const totalQuestions = pkg?.sections.reduce((sum: number, sec) => sum + sec.questions.length, 0) ?? 0;
|
| 47 |
+
const answeredCount = Object.keys(answers).length;
|
| 48 |
+
|
| 49 |
+
const handleFinishWithTracking = useCallback(async () => {
|
| 50 |
+
const result = await handleFinish();
|
| 51 |
+
if (result) {
|
| 52 |
+
trackUmamiEvent(AnalyticsEvent.ATTEMPT_FINISH, {
|
| 53 |
+
exam_type: pkg?.examTypeName ?? "unknown",
|
| 54 |
+
score: result.totalScore,
|
| 55 |
+
max_score: result.maxScore,
|
| 56 |
+
percentage: result.percentage,
|
| 57 |
+
time_elapsed_sec: timeElapsed,
|
| 58 |
+
});
|
| 59 |
+
}
|
| 60 |
+
}, [handleFinish, pkg?.examTypeName, timeElapsed]);
|
| 61 |
+
|
| 62 |
+
const handleAbandonWithTracking = useCallback(async () => {
|
| 63 |
+
await handleAbandon();
|
| 64 |
+
trackUmamiEvent(AnalyticsEvent.ATTEMPT_ABANDON, {
|
| 65 |
+
exam_type: pkg?.examTypeName ?? "unknown",
|
| 66 |
+
questions_answered: answeredCount,
|
| 67 |
+
total_questions: totalQuestions,
|
| 68 |
+
});
|
| 69 |
+
}, [handleAbandon, pkg?.examTypeName, answeredCount, totalQuestions]);
|
| 70 |
+
|
| 71 |
if (packageQuery.isLoading) {
|
| 72 |
return (
|
| 73 |
<div className="min-h-screen pt-8 pb-32 px-6 md:px-12 lg:px-16 max-w-4xl mx-auto bg-[var(--warm-cream)]">
|
|
|
|
| 91 |
);
|
| 92 |
}
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
const currentSection = pkg.sections[currentSectionIdx];
|
| 95 |
if (!currentSection) {
|
| 96 |
return (
|
apps/web/src/routes/package.$id.take.tsx
CHANGED
|
@@ -67,61 +67,61 @@ function TakeTestComponent() {
|
|
| 67 |
startQuestionTimer,
|
| 68 |
} = useTestSession(packageId);
|
| 69 |
|
| 70 |
-
|
| 71 |
-
return (
|
| 72 |
-
<div className="min-h-screen pt-8 pb-32 px-6 md:px-12 lg:px-16 max-w-4xl mx-auto bg-[var(--warm-cream)]">
|
| 73 |
-
<div className="h-8 w-48 bg-[var(--oat-light)] animate-pulse rounded mb-4" />
|
| 74 |
-
<div className="h-64 bg-[var(--oat-light)] animate-pulse rounded-[var(--radius-xl)]" />
|
| 75 |
-
</div>
|
| 76 |
-
);
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
if (!pkg) {
|
| 80 |
-
return (
|
| 81 |
-
<div className="min-h-screen pt-8 pb-32 px-6 md:px-12 lg:px-16 max-w-4xl mx-auto bg-[var(--warm-cream)]">
|
| 82 |
-
<div className="text-center py-20">
|
| 83 |
-
<MaterialIcon name="error_outline" className="text-6xl text-[var(--warm-silver)] mx-auto mb-4" />
|
| 84 |
-
<p className="text-lg text-[var(--warm-charcoal)] font-semibold">Paket tidak ditemukan</p>
|
| 85 |
-
<Link to="/packages" className="text-[var(--matcha-600)] font-semibold mt-4 inline-block">
|
| 86 |
-
Kembali ke Paket
|
| 87 |
-
</Link>
|
| 88 |
-
</div>
|
| 89 |
-
</div>
|
| 90 |
-
);
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
const totalQuestions = pkg.sections.reduce((sum: number, sec) => sum + sec.questions.length, 0);
|
| 94 |
const answeredCount = Object.keys(answers).length;
|
| 95 |
|
| 96 |
const handleStartWithTracking = useCallback(async () => {
|
| 97 |
await handleStart();
|
| 98 |
trackUmamiEvent(AnalyticsEvent.ATTEMPT_START, {
|
| 99 |
-
exam_type: pkg.examTypeName ?? "unknown",
|
| 100 |
question_count: totalQuestions,
|
| 101 |
});
|
| 102 |
-
}, [handleStart, pkg.examTypeName, totalQuestions]);
|
| 103 |
|
| 104 |
const handleFinishWithTracking = useCallback(async () => {
|
| 105 |
const result = await handleFinish();
|
| 106 |
if (result) {
|
| 107 |
trackUmamiEvent(AnalyticsEvent.ATTEMPT_FINISH, {
|
| 108 |
-
exam_type: pkg.examTypeName ?? "unknown",
|
| 109 |
score: result.totalScore,
|
| 110 |
max_score: result.maxScore,
|
| 111 |
percentage: result.percentage,
|
| 112 |
time_elapsed_sec: timeElapsed,
|
| 113 |
});
|
| 114 |
}
|
| 115 |
-
}, [handleFinish, pkg.examTypeName, timeElapsed]);
|
| 116 |
|
| 117 |
const handleAbandonWithTracking = useCallback(async () => {
|
| 118 |
await handleAbandon();
|
| 119 |
trackUmamiEvent(AnalyticsEvent.ATTEMPT_ABANDON, {
|
| 120 |
-
exam_type: pkg.examTypeName ?? "unknown",
|
| 121 |
questions_answered: answeredCount,
|
| 122 |
total_questions: totalQuestions,
|
| 123 |
});
|
| 124 |
-
}, [handleAbandon, pkg.examTypeName, answeredCount, totalQuestions]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
// Start screen
|
| 127 |
if (!isStarted) {
|
|
|
|
| 67 |
startQuestionTimer,
|
| 68 |
} = useTestSession(packageId);
|
| 69 |
|
| 70 |
+
const totalQuestions = pkg?.sections.reduce((sum: number, sec) => sum + sec.questions.length, 0) ?? 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
const answeredCount = Object.keys(answers).length;
|
| 72 |
|
| 73 |
const handleStartWithTracking = useCallback(async () => {
|
| 74 |
await handleStart();
|
| 75 |
trackUmamiEvent(AnalyticsEvent.ATTEMPT_START, {
|
| 76 |
+
exam_type: pkg?.examTypeName ?? "unknown",
|
| 77 |
question_count: totalQuestions,
|
| 78 |
});
|
| 79 |
+
}, [handleStart, pkg?.examTypeName, totalQuestions]);
|
| 80 |
|
| 81 |
const handleFinishWithTracking = useCallback(async () => {
|
| 82 |
const result = await handleFinish();
|
| 83 |
if (result) {
|
| 84 |
trackUmamiEvent(AnalyticsEvent.ATTEMPT_FINISH, {
|
| 85 |
+
exam_type: pkg?.examTypeName ?? "unknown",
|
| 86 |
score: result.totalScore,
|
| 87 |
max_score: result.maxScore,
|
| 88 |
percentage: result.percentage,
|
| 89 |
time_elapsed_sec: timeElapsed,
|
| 90 |
});
|
| 91 |
}
|
| 92 |
+
}, [handleFinish, pkg?.examTypeName, timeElapsed]);
|
| 93 |
|
| 94 |
const handleAbandonWithTracking = useCallback(async () => {
|
| 95 |
await handleAbandon();
|
| 96 |
trackUmamiEvent(AnalyticsEvent.ATTEMPT_ABANDON, {
|
| 97 |
+
exam_type: pkg?.examTypeName ?? "unknown",
|
| 98 |
questions_answered: answeredCount,
|
| 99 |
total_questions: totalQuestions,
|
| 100 |
});
|
| 101 |
+
}, [handleAbandon, pkg?.examTypeName, answeredCount, totalQuestions]);
|
| 102 |
+
|
| 103 |
+
if (packageQuery.isLoading || activeAttemptQuery.isLoading) {
|
| 104 |
+
return (
|
| 105 |
+
<div className="min-h-screen pt-8 pb-32 px-6 md:px-12 lg:px-16 max-w-4xl mx-auto bg-[var(--warm-cream)]">
|
| 106 |
+
<div className="h-8 w-48 bg-[var(--oat-light)] animate-pulse rounded mb-4" />
|
| 107 |
+
<div className="h-64 bg-[var(--oat-light)] animate-pulse rounded-[var(--radius-xl)]" />
|
| 108 |
+
</div>
|
| 109 |
+
);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
if (!pkg) {
|
| 113 |
+
return (
|
| 114 |
+
<div className="min-h-screen pt-8 pb-32 px-6 md:px-12 lg:px-16 max-w-4xl mx-auto bg-[var(--warm-cream)]">
|
| 115 |
+
<div className="text-center py-20">
|
| 116 |
+
<MaterialIcon name="error_outline" className="text-6xl text-[var(--warm-silver)] mx-auto mb-4" />
|
| 117 |
+
<p className="text-lg text-[var(--warm-charcoal)] font-semibold">Paket tidak ditemukan</p>
|
| 118 |
+
<Link to="/packages" className="text-[var(--matcha-600)] font-semibold mt-4 inline-block">
|
| 119 |
+
Kembali ke Paket
|
| 120 |
+
</Link>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
);
|
| 124 |
+
}
|
| 125 |
|
| 126 |
// Start screen
|
| 127 |
if (!isStarted) {
|