@schoolio/player 1.1.1 → 1.2.1

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/README.md CHANGED
@@ -1,6 +1,11 @@
1
1
  # @schoolio/player
2
2
 
3
- A React component for loading and playing quizzes from Quiz Engine.
3
+ React components for loading quizzes and displaying attempt results from Quiz Engine.
4
+
5
+ ## Components
6
+
7
+ - **QuizPlayer** - Interactive quiz-taking component
8
+ - **AttemptViewer** - Displays quiz attempt results with detailed breakdown
4
9
 
5
10
  ## Installation
6
11
 
@@ -138,6 +143,90 @@ await client.updateAttempt(attempt.id, {
138
143
  });
139
144
  ```
140
145
 
146
+ ---
147
+
148
+ ## AttemptViewer
149
+
150
+ The `AttemptViewer` component displays quiz attempt results with a summary and detailed question-by-question breakdown.
151
+
152
+ ### Usage
153
+
154
+ ```tsx
155
+ import { AttemptViewer } from '@schoolio/player';
156
+
157
+ function MyResultsPage() {
158
+ return (
159
+ <AttemptViewer
160
+ attemptId={123}
161
+ apiBaseUrl="https://your-quiz-engine-url.com"
162
+ />
163
+ );
164
+ }
165
+ ```
166
+
167
+ ### Props
168
+
169
+ | Prop | Type | Required | Description |
170
+ |------|------|----------|-------------|
171
+ | `attemptId` | `number` | Yes | The ID of the quiz attempt to display |
172
+ | `apiBaseUrl` | `string` | Yes | Base URL of your Quiz Engine API |
173
+ | `title` | `string` | No | Optional title (reserved for future use) |
174
+
175
+ ### API Endpoint Required
176
+
177
+ The component expects this endpoint:
178
+
179
+ ```
180
+ GET {apiBaseUrl}/api/quiz-attempts/{attemptId}/results
181
+ ```
182
+
183
+ **Response format:**
184
+
185
+ ```json
186
+ {
187
+ "attempt": {
188
+ "id": 123,
189
+ "quizId": 1,
190
+ "score": 80,
191
+ "totalQuestions": 10,
192
+ "correctAnswers": 8,
193
+ "timeTaken": 300,
194
+ "completedAt": "2025-01-02T10:00:00Z",
195
+ "answers": { "1": "selected_answer", "2": "another_answer" }
196
+ },
197
+ "questions": [
198
+ {
199
+ "id": 1,
200
+ "questionText": "What is 2+2?",
201
+ "questionType": "multiple_choice",
202
+ "options": ["3", "4", "5"],
203
+ "correctAnswer": "4",
204
+ "explanation": "Basic addition"
205
+ }
206
+ ]
207
+ }
208
+ ```
209
+
210
+ ### Styling
211
+
212
+ The component uses 100% width to fill its parent container. Wrap it in a sized container to control dimensions:
213
+
214
+ ```tsx
215
+ <div style={{ maxWidth: '800px', margin: '0 auto' }}>
216
+ <AttemptViewer attemptId={123} apiBaseUrl="https://api.example.com" />
217
+ </div>
218
+ ```
219
+
220
+ ### Features
221
+
222
+ - **Summary cards** showing score, questions count, and time taken
223
+ - **Detailed breakdown** of each question with correct/incorrect indicators
224
+ - **Explanations** displayed for each question when available
225
+ - **Purple theme** (#6721b0) matching the QuizPlayer design
226
+ - **Responsive layout** that adapts to container width
227
+
228
+ ---
229
+
141
230
  ## License
142
231
 
143
232
  MIT
package/dist/index.d.mts CHANGED
@@ -94,9 +94,20 @@ interface QuizPlayerStyles {
94
94
  buttonClassName?: string;
95
95
  progressClassName?: string;
96
96
  }
97
+ interface AttemptViewerProps {
98
+ attemptId: string;
99
+ apiBaseUrl: string;
100
+ authToken?: string;
101
+ onError?: (error: Error) => void;
102
+ className?: string;
103
+ showExplanations?: boolean;
104
+ title?: string;
105
+ }
97
106
 
98
107
  declare function QuizPlayer({ quizId, lessonId, assignLessonId, courseId, childId, parentId, apiBaseUrl, authToken, onComplete, onError, onProgress, className, }: QuizPlayerProps): react_jsx_runtime.JSX.Element;
99
108
 
109
+ declare function AttemptViewer({ attemptId, apiBaseUrl, authToken, onError, className, showExplanations, title, }: AttemptViewerProps): react_jsx_runtime.JSX.Element;
110
+
100
111
  interface ApiClientConfig {
101
112
  baseUrl: string;
102
113
  authToken?: string;
@@ -143,4 +154,4 @@ declare function calculateScore(answers: QuizAnswerDetail[]): {
143
154
  };
144
155
  declare function formatTime(seconds: number): string;
145
156
 
146
- export { type ApiClientConfig, type AttemptStatus, type ExternalQuizAttempt, type QuestionType, type Quiz, type QuizAnswerDetail, QuizApiClient, QuizPlayer, type QuizPlayerProps, type QuizPlayerStyles, type QuizProgress, type QuizQuestion, type QuizResult, calculateScore, checkAnswer, createAnswerDetail, formatTime };
157
+ export { type ApiClientConfig, type AttemptStatus, AttemptViewer, type AttemptViewerProps, type ExternalQuizAttempt, type QuestionType, type Quiz, type QuizAnswerDetail, QuizApiClient, QuizPlayer, type QuizPlayerProps, type QuizPlayerStyles, type QuizProgress, type QuizQuestion, type QuizResult, calculateScore, checkAnswer, createAnswerDetail, formatTime };
package/dist/index.d.ts CHANGED
@@ -94,9 +94,20 @@ interface QuizPlayerStyles {
94
94
  buttonClassName?: string;
95
95
  progressClassName?: string;
96
96
  }
97
+ interface AttemptViewerProps {
98
+ attemptId: string;
99
+ apiBaseUrl: string;
100
+ authToken?: string;
101
+ onError?: (error: Error) => void;
102
+ className?: string;
103
+ showExplanations?: boolean;
104
+ title?: string;
105
+ }
97
106
 
98
107
  declare function QuizPlayer({ quizId, lessonId, assignLessonId, courseId, childId, parentId, apiBaseUrl, authToken, onComplete, onError, onProgress, className, }: QuizPlayerProps): react_jsx_runtime.JSX.Element;
99
108
 
109
+ declare function AttemptViewer({ attemptId, apiBaseUrl, authToken, onError, className, showExplanations, title, }: AttemptViewerProps): react_jsx_runtime.JSX.Element;
110
+
100
111
  interface ApiClientConfig {
101
112
  baseUrl: string;
102
113
  authToken?: string;
@@ -143,4 +154,4 @@ declare function calculateScore(answers: QuizAnswerDetail[]): {
143
154
  };
144
155
  declare function formatTime(seconds: number): string;
145
156
 
146
- export { type ApiClientConfig, type AttemptStatus, type ExternalQuizAttempt, type QuestionType, type Quiz, type QuizAnswerDetail, QuizApiClient, QuizPlayer, type QuizPlayerProps, type QuizPlayerStyles, type QuizProgress, type QuizQuestion, type QuizResult, calculateScore, checkAnswer, createAnswerDetail, formatTime };
157
+ export { type ApiClientConfig, type AttemptStatus, AttemptViewer, type AttemptViewerProps, type ExternalQuizAttempt, type QuestionType, type Quiz, type QuizAnswerDetail, QuizApiClient, QuizPlayer, type QuizPlayerProps, type QuizPlayerStyles, type QuizProgress, type QuizQuestion, type QuizResult, calculateScore, checkAnswer, createAnswerDetail, formatTime };
package/dist/index.js CHANGED
@@ -614,7 +614,309 @@ function QuizPlayer({
614
614
  ] })
615
615
  ] });
616
616
  }
617
+ var defaultStyles2 = {
618
+ container: {
619
+ fontFamily: "system-ui, -apple-system, sans-serif",
620
+ width: "100%",
621
+ padding: "20px",
622
+ backgroundColor: "#ffffff",
623
+ borderRadius: "12px",
624
+ boxSizing: "border-box"
625
+ },
626
+ header: {
627
+ marginBottom: "24px",
628
+ borderBottom: "1px solid #e5e7eb",
629
+ paddingBottom: "20px"
630
+ },
631
+ summaryGrid: {
632
+ display: "grid",
633
+ gridTemplateColumns: "repeat(auto-fit, minmax(120px, 1fr))",
634
+ gap: "16px"
635
+ },
636
+ summaryCard: {
637
+ padding: "12px",
638
+ backgroundColor: "#f9fafb",
639
+ borderRadius: "8px",
640
+ textAlign: "center"
641
+ },
642
+ summaryValue: {
643
+ fontSize: "20px",
644
+ fontWeight: "600",
645
+ color: "#6721b0",
646
+ marginBottom: "2px"
647
+ },
648
+ summaryLabel: {
649
+ fontSize: "10px",
650
+ color: "#6b7280",
651
+ textTransform: "uppercase",
652
+ letterSpacing: "0.05em"
653
+ },
654
+ questionsList: {
655
+ display: "flex",
656
+ flexDirection: "column",
657
+ gap: "16px"
658
+ },
659
+ questionCard: {
660
+ padding: "16px",
661
+ border: "2px solid #e5e7eb",
662
+ borderRadius: "8px"
663
+ },
664
+ questionCardCorrect: {
665
+ borderColor: "#22c55e",
666
+ backgroundColor: "#f0fdf4"
667
+ },
668
+ questionCardIncorrect: {
669
+ borderColor: "#ef4444",
670
+ backgroundColor: "#fef2f2"
671
+ },
672
+ questionHeader: {
673
+ display: "flex",
674
+ justifyContent: "space-between",
675
+ alignItems: "flex-start",
676
+ marginBottom: "12px"
677
+ },
678
+ questionNumber: {
679
+ fontSize: "12px",
680
+ color: "#6b7280",
681
+ fontWeight: "500"
682
+ },
683
+ questionBadge: {
684
+ padding: "4px 8px",
685
+ borderRadius: "4px",
686
+ fontSize: "12px",
687
+ fontWeight: "600"
688
+ },
689
+ badgeCorrect: {
690
+ backgroundColor: "#dcfce7",
691
+ color: "#166534"
692
+ },
693
+ badgeIncorrect: {
694
+ backgroundColor: "#fee2e2",
695
+ color: "#991b1b"
696
+ },
697
+ questionText: {
698
+ fontSize: "16px",
699
+ fontWeight: "500",
700
+ marginBottom: "12px",
701
+ color: "#111827"
702
+ },
703
+ answerSection: {
704
+ fontSize: "14px",
705
+ marginBottom: "8px"
706
+ },
707
+ answerLabel: {
708
+ fontWeight: "600",
709
+ color: "#6b7280",
710
+ marginRight: "8px"
711
+ },
712
+ studentAnswer: {
713
+ color: "#111827"
714
+ },
715
+ correctAnswer: {
716
+ color: "#166534"
717
+ },
718
+ points: {
719
+ fontSize: "13px",
720
+ color: "#6b7280",
721
+ marginTop: "8px"
722
+ },
723
+ explanation: {
724
+ marginTop: "12px",
725
+ padding: "12px",
726
+ backgroundColor: "#f3e8ff",
727
+ borderRadius: "6px",
728
+ fontSize: "14px",
729
+ color: "#581c87"
730
+ },
731
+ loading: {
732
+ textAlign: "center",
733
+ padding: "40px 20px"
734
+ },
735
+ spinner: {
736
+ display: "inline-block",
737
+ width: "32px",
738
+ height: "32px",
739
+ border: "3px solid #e5e7eb",
740
+ borderTopColor: "#6721b0",
741
+ borderRadius: "50%",
742
+ animation: "spin 1s linear infinite"
743
+ },
744
+ error: {
745
+ textAlign: "center",
746
+ padding: "40px 20px",
747
+ color: "#ef4444"
748
+ },
749
+ retryButton: {
750
+ marginTop: "16px",
751
+ padding: "12px 24px",
752
+ backgroundColor: "#6721b0",
753
+ color: "#ffffff",
754
+ border: "none",
755
+ borderRadius: "8px",
756
+ fontSize: "16px",
757
+ fontWeight: "500",
758
+ cursor: "pointer"
759
+ }
760
+ };
761
+ var spinnerKeyframes = `
762
+ @keyframes spin {
763
+ to { transform: rotate(360deg); }
764
+ }
765
+ `;
766
+ function formatAnswer(answer) {
767
+ if (answer === null || answer === void 0) {
768
+ return "No answer";
769
+ }
770
+ if (typeof answer === "string") {
771
+ return answer;
772
+ }
773
+ if (Array.isArray(answer)) {
774
+ return answer.join(", ");
775
+ }
776
+ if (typeof answer === "object") {
777
+ return Object.entries(answer).map(([k, v]) => `${k} \u2192 ${v}`).join(", ");
778
+ }
779
+ return String(answer);
780
+ }
781
+ function AttemptViewer({
782
+ attemptId,
783
+ apiBaseUrl,
784
+ authToken,
785
+ onError,
786
+ className,
787
+ showExplanations = true,
788
+ title
789
+ }) {
790
+ const [attempt, setAttempt] = react.useState(null);
791
+ const [loading, setLoading] = react.useState(true);
792
+ const [error, setError] = react.useState(null);
793
+ react.useEffect(() => {
794
+ new QuizApiClient({
795
+ baseUrl: apiBaseUrl,
796
+ authToken
797
+ });
798
+ async function fetchAttempt() {
799
+ setLoading(true);
800
+ setError(null);
801
+ try {
802
+ const response = await fetch(`${apiBaseUrl}/api/external/quiz-attempts/${attemptId}`, {
803
+ headers: authToken ? { Authorization: `Bearer ${authToken}` } : {}
804
+ });
805
+ if (!response.ok) {
806
+ throw new Error(`Failed to fetch attempt: ${response.statusText}`);
807
+ }
808
+ const data = await response.json();
809
+ setAttempt(data);
810
+ } catch (err) {
811
+ const errorMessage = err instanceof Error ? err.message : "Failed to load attempt";
812
+ setError(errorMessage);
813
+ onError?.(err instanceof Error ? err : new Error(errorMessage));
814
+ } finally {
815
+ setLoading(false);
816
+ }
817
+ }
818
+ fetchAttempt();
819
+ }, [attemptId, apiBaseUrl, authToken, onError]);
820
+ const handleRetry = () => {
821
+ setLoading(true);
822
+ setError(null);
823
+ window.location.reload();
824
+ };
825
+ if (loading) {
826
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.container, className, children: [
827
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: spinnerKeyframes }),
828
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.loading, children: [
829
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: defaultStyles2.spinner }),
830
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { marginTop: "16px", color: "#6b7280" }, children: "Loading attempt..." })
831
+ ] })
832
+ ] });
833
+ }
834
+ if (error || !attempt) {
835
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: defaultStyles2.container, className, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.error, children: [
836
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { fontSize: "18px", fontWeight: "500" }, children: "Failed to load attempt" }),
837
+ /* @__PURE__ */ jsxRuntime.jsx("p", { style: { marginTop: "8px", color: "#6b7280" }, children: error }),
838
+ /* @__PURE__ */ jsxRuntime.jsx("button", { style: defaultStyles2.retryButton, onClick: handleRetry, children: "Try Again" })
839
+ ] }) });
840
+ }
841
+ const scorePercentage = attempt.score ?? 0;
842
+ const correctCount = attempt.correctAnswers ?? 0;
843
+ const totalQuestions = attempt.totalQuestions;
844
+ const timeSpent = attempt.timeSpentSeconds ?? 0;
845
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.container, className, children: [
846
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: spinnerKeyframes }),
847
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: defaultStyles2.header, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.summaryGrid, children: [
848
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.summaryCard, children: [
849
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.summaryValue, children: [
850
+ scorePercentage,
851
+ "%"
852
+ ] }),
853
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: defaultStyles2.summaryLabel, children: "Score" })
854
+ ] }),
855
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.summaryCard, children: [
856
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.summaryValue, children: [
857
+ correctCount,
858
+ "/",
859
+ totalQuestions
860
+ ] }),
861
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: defaultStyles2.summaryLabel, children: "Correct" })
862
+ ] }),
863
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.summaryCard, children: [
864
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: defaultStyles2.summaryValue, children: formatTime(timeSpent) }),
865
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: defaultStyles2.summaryLabel, children: "Time" })
866
+ ] })
867
+ ] }) }),
868
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: defaultStyles2.questionsList, children: attempt.answers.map((answer, index) => /* @__PURE__ */ jsxRuntime.jsxs(
869
+ "div",
870
+ {
871
+ style: {
872
+ ...defaultStyles2.questionCard,
873
+ ...answer.isCorrect ? defaultStyles2.questionCardCorrect : defaultStyles2.questionCardIncorrect
874
+ },
875
+ children: [
876
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.questionHeader, children: [
877
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: defaultStyles2.questionNumber, children: [
878
+ "Question ",
879
+ index + 1
880
+ ] }),
881
+ /* @__PURE__ */ jsxRuntime.jsx(
882
+ "span",
883
+ {
884
+ style: {
885
+ ...defaultStyles2.questionBadge,
886
+ ...answer.isCorrect ? defaultStyles2.badgeCorrect : defaultStyles2.badgeIncorrect
887
+ },
888
+ children: answer.isCorrect ? "Correct" : "Incorrect"
889
+ }
890
+ )
891
+ ] }),
892
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: defaultStyles2.questionText, children: answer.questionText }),
893
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.answerSection, children: [
894
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: defaultStyles2.answerLabel, children: "Your answer:" }),
895
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: defaultStyles2.studentAnswer, children: formatAnswer(answer.selectedAnswer) })
896
+ ] }),
897
+ !answer.isCorrect && answer.correctAnswer && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.answerSection, children: [
898
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: defaultStyles2.answerLabel, children: "Correct answer:" }),
899
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: defaultStyles2.correctAnswer, children: formatAnswer(answer.correctAnswer) })
900
+ ] }),
901
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.points, children: [
902
+ answer.pointsEarned,
903
+ " / ",
904
+ answer.points,
905
+ " points"
906
+ ] }),
907
+ showExplanations && answer.explanation && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles2.explanation, children: [
908
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Explanation:" }),
909
+ " ",
910
+ answer.explanation
911
+ ] })
912
+ ]
913
+ },
914
+ answer.questionId
915
+ )) })
916
+ ] });
917
+ }
617
918
 
919
+ exports.AttemptViewer = AttemptViewer;
618
920
  exports.QuizApiClient = QuizApiClient;
619
921
  exports.QuizPlayer = QuizPlayer;
620
922
  exports.calculateScore = calculateScore;