react-native-elearn-ui 0.1.7 → 0.1.9

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.
Files changed (46) hide show
  1. package/lib/module/components/Icon.js +27 -0
  2. package/lib/module/components/Icon.js.map +1 -1
  3. package/lib/module/index.js +1 -1
  4. package/lib/module/index.js.map +1 -1
  5. package/lib/module/screens/PearlsScreen.js +741 -0
  6. package/lib/module/screens/PearlsScreen.js.map +1 -0
  7. package/lib/module/screens/QbankScreen.js +18 -3
  8. package/lib/module/screens/QbankScreen.js.map +1 -1
  9. package/lib/module/screens/QuestionScreen.js +22 -10
  10. package/lib/module/screens/QuestionScreen.js.map +1 -1
  11. package/lib/module/screens/QuizStartScreen.js +55 -7
  12. package/lib/module/screens/QuizStartScreen.js.map +1 -1
  13. package/lib/module/screens/ResultScreen.js +10 -6
  14. package/lib/module/screens/ResultScreen.js.map +1 -1
  15. package/lib/module/screens/TopicListScreen.js +53 -6
  16. package/lib/module/screens/TopicListScreen.js.map +1 -1
  17. package/lib/module/screens/index.js +1 -0
  18. package/lib/module/screens/index.js.map +1 -1
  19. package/lib/typescript/src/components/Icon.d.ts +1 -1
  20. package/lib/typescript/src/components/Icon.d.ts.map +1 -1
  21. package/lib/typescript/src/index.d.ts +1 -1
  22. package/lib/typescript/src/index.d.ts.map +1 -1
  23. package/lib/typescript/src/screens/PearlsScreen.d.ts +8 -0
  24. package/lib/typescript/src/screens/PearlsScreen.d.ts.map +1 -0
  25. package/lib/typescript/src/screens/QbankScreen.d.ts +3 -0
  26. package/lib/typescript/src/screens/QbankScreen.d.ts.map +1 -1
  27. package/lib/typescript/src/screens/QuestionScreen.d.ts +6 -4
  28. package/lib/typescript/src/screens/QuestionScreen.d.ts.map +1 -1
  29. package/lib/typescript/src/screens/QuizStartScreen.d.ts +6 -3
  30. package/lib/typescript/src/screens/QuizStartScreen.d.ts.map +1 -1
  31. package/lib/typescript/src/screens/ResultScreen.d.ts +2 -1
  32. package/lib/typescript/src/screens/ResultScreen.d.ts.map +1 -1
  33. package/lib/typescript/src/screens/TopicListScreen.d.ts +4 -2
  34. package/lib/typescript/src/screens/TopicListScreen.d.ts.map +1 -1
  35. package/lib/typescript/src/screens/index.d.ts +1 -0
  36. package/lib/typescript/src/screens/index.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/components/Icon.tsx +35 -1
  39. package/src/index.tsx +1 -0
  40. package/src/screens/PearlsScreen.tsx +720 -0
  41. package/src/screens/QbankScreen.tsx +24 -3
  42. package/src/screens/QuestionScreen.tsx +28 -14
  43. package/src/screens/QuizStartScreen.tsx +45 -10
  44. package/src/screens/ResultScreen.tsx +13 -8
  45. package/src/screens/TopicListScreen.tsx +40 -7
  46. package/src/screens/index.ts +1 -0
@@ -12,17 +12,32 @@ import { useElearnConfig } from '../context/ElearnConfigContext';
12
12
  import { useTheme } from '../context/ThemeContext';
13
13
  import { Typography, Card, ProgressBar, Icon } from '../components';
14
14
  import type { IconName } from '../components';
15
+ import type { Course } from '../types';
15
16
 
16
17
  interface QbankScreenProps {
17
18
  hideHeader?: boolean;
19
+ onBack?: () => void;
20
+ onSelectCourse?: (course: Course) => void;
18
21
  }
19
22
 
20
- export const QbankScreen: React.FC<QbankScreenProps> = ({ hideHeader = false }) => {
23
+ export const QbankScreen: React.FC<QbankScreenProps> = ({
24
+ hideHeader = false,
25
+ onBack,
26
+ onSelectCourse,
27
+ }) => {
21
28
  const { config, callbacks } = useElearnConfig();
22
29
  const theme = useTheme();
23
30
 
24
31
  const courses = config.courses || [];
25
32
 
33
+ const handleBack = () => {
34
+ if (onBack) {
35
+ onBack();
36
+ } else {
37
+ callbacks.onBackToDashboard?.();
38
+ }
39
+ };
40
+
26
41
  return (
27
42
  <SafeAreaView style={[styles.safeArea, { backgroundColor: theme.background }]}>
28
43
  <StatusBar barStyle="dark-content" />
@@ -31,7 +46,7 @@ export const QbankScreen: React.FC<QbankScreenProps> = ({ hideHeader = false })
31
46
  {!hideHeader && (
32
47
  <View style={styles.header}>
33
48
  <TouchableOpacity
34
- onPress={() => callbacks.onBackToDashboard?.()}
49
+ onPress={handleBack}
35
50
  style={[styles.backButton, { borderColor: theme.border }]}
36
51
  >
37
52
  <Icon name="arrow-left" size={20} color={theme.textPrimary} />
@@ -145,7 +160,13 @@ export const QbankScreen: React.FC<QbankScreenProps> = ({ hideHeader = false })
145
160
  <Card
146
161
  key={course.id}
147
162
  bordered
148
- onPress={() => callbacks.onSelectCourse?.(course)}
163
+ onPress={() => {
164
+ if (onSelectCourse) {
165
+ onSelectCourse(course);
166
+ } else {
167
+ callbacks.onSelectCourse?.(course);
168
+ }
169
+ }}
149
170
  style={styles.subjectCard}
150
171
  >
151
172
  <View style={styles.subjectRow}>
@@ -14,22 +14,32 @@ import { useTheme } from '../context/ThemeContext';
14
14
  import { Typography, Button, Icon } from '../components';
15
15
 
16
16
  interface QuestionScreenProps {
17
- quizId: string;
18
- questions: Question[];
19
- title: string;
17
+ quizId?: string;
18
+ questions?: Question[];
19
+ title?: string;
20
+ topicId?: string;
20
21
  onClose: () => void;
22
+ onFinishQuiz?: (quizId: string, results: QuizResults) => void;
21
23
  hideHeader?: boolean;
22
24
  }
23
25
 
24
26
  export const QuestionScreen: React.FC<QuestionScreenProps> = ({
25
27
  quizId,
26
28
  questions,
29
+ title,
30
+ topicId,
27
31
  onClose,
32
+ onFinishQuiz,
28
33
  hideHeader = false,
29
34
  }) => {
30
- const { callbacks } = useElearnConfig();
35
+ const { config, callbacks } = useElearnConfig();
31
36
  const theme = useTheme();
32
37
 
38
+ const activeTopic = topicId ? config.courses?.flatMap((c) => c.topics).find((t) => t.id === topicId) : undefined;
39
+ const activeQuestions = questions || activeTopic?.questions || [];
40
+ const activeTitle = title || activeTopic?.title || 'Quiz';
41
+ const activeQuizId = quizId || topicId || '';
42
+
33
43
  const [currentIndex, setCurrentIndex] = useState(0);
34
44
  const [selectedOption, setSelectedOption] = useState<number | null>(null);
35
45
  const [isAnswered, setIsAnswered] = useState(false);
@@ -38,7 +48,7 @@ export const QuestionScreen: React.FC<QuestionScreenProps> = ({
38
48
  const [startTime] = useState(Date.now());
39
49
  const [isPaused, setIsPaused] = useState(false);
40
50
 
41
- if (!questions || questions.length === 0) {
51
+ if (!activeQuestions || activeQuestions.length === 0) {
42
52
  return (
43
53
  <SafeAreaView style={[styles.safeArea, { backgroundColor: theme.background }]}>
44
54
  <View style={styles.errorContainer}>
@@ -49,7 +59,7 @@ export const QuestionScreen: React.FC<QuestionScreenProps> = ({
49
59
  );
50
60
  }
51
61
 
52
- const currentQuestion = questions[currentIndex]!;
62
+ const currentQuestion = activeQuestions[currentIndex]!;
53
63
  const optionPrefixes = ['A', 'B', 'C', 'D', 'E', 'F'];
54
64
 
55
65
  const handleOptionPress = (index: number) => {
@@ -68,19 +78,19 @@ export const QuestionScreen: React.FC<QuestionScreenProps> = ({
68
78
  };
69
79
 
70
80
  const handleNext = () => {
71
- if (currentIndex < questions.length - 1) {
81
+ if (currentIndex < activeQuestions.length - 1) {
72
82
  setCurrentIndex((prev) => prev + 1);
73
83
  setSelectedOption(null);
74
84
  setIsAnswered(false);
75
85
  } else {
76
86
  // Last question - finish quiz
77
87
  const totalTimeSeconds = Math.round((Date.now() - startTime) / 1000);
78
- const wrongCount = questions.length - correctCount;
79
- const accuracy = Math.round((correctCount / questions.length) * 100);
88
+ const wrongCount = activeQuestions.length - correctCount;
89
+ const accuracy = Math.round((correctCount / activeQuestions.length) * 100);
80
90
 
81
91
  const results: QuizResults = {
82
- quizId,
83
- totalQuestions: questions.length,
92
+ quizId: activeQuizId,
93
+ totalQuestions: activeQuestions.length,
84
94
  correctCount,
85
95
  wrongCount,
86
96
  skippedCount: 0,
@@ -89,7 +99,11 @@ export const QuestionScreen: React.FC<QuestionScreenProps> = ({
89
99
  userAnswers,
90
100
  };
91
101
 
92
- callbacks.onFinishQuiz?.(quizId, results);
102
+ if (onFinishQuiz) {
103
+ onFinishQuiz(activeQuizId, results);
104
+ } else {
105
+ callbacks.onFinishQuiz?.(activeQuizId, results);
106
+ }
93
107
  }
94
108
  };
95
109
 
@@ -105,7 +119,7 @@ export const QuestionScreen: React.FC<QuestionScreenProps> = ({
105
119
  </TouchableOpacity>
106
120
 
107
121
  <Typography variant="h2" bold style={styles.headerTitle}>
108
- QBank
122
+ {activeTitle || 'QBank'}
109
123
  </Typography>
110
124
 
111
125
  <TouchableOpacity style={styles.shareButton}>
@@ -119,7 +133,7 @@ export const QuestionScreen: React.FC<QuestionScreenProps> = ({
119
133
  {/* Question Header & Pause/Bookmark Actions Row */}
120
134
  <View style={styles.metaRow}>
121
135
  <Typography variant="caption" bold color={theme.textSecondary} style={styles.questionNumLabel}>
122
- QUESTION {currentIndex + 1}/{questions.length}
136
+ QUESTION {currentIndex + 1}/{activeQuestions.length}
123
137
  </Typography>
124
138
  <View style={styles.metaActions}>
125
139
  <TouchableOpacity style={styles.metaActionBtn}>
@@ -6,32 +6,67 @@ import {
6
6
  SafeAreaView,
7
7
  StatusBar,
8
8
  } from 'react-native';
9
- import type { Topic, Course } from '../types';
9
+ import type { Topic, Course, Question } from '../types';
10
10
  import { useElearnConfig } from '../context/ElearnConfigContext';
11
11
  import { useTheme } from '../context/ThemeContext';
12
12
  import { Typography, Card, Button, Icon } from '../components';
13
13
 
14
14
  interface QuizStartScreenProps {
15
- course: Course;
16
- topic: Topic;
15
+ course?: Course;
16
+ courseId?: string;
17
+ topic?: Topic;
18
+ topicId?: string;
17
19
  onBack: () => void;
18
20
  hideHeader?: boolean;
21
+ onStartQuiz?: (quizId: string, questions: Question[], title: string) => void;
19
22
  }
20
23
 
21
24
  export const QuizStartScreen: React.FC<QuizStartScreenProps> = ({
25
+ course,
26
+ courseId,
22
27
  topic,
28
+ topicId,
23
29
  onBack,
24
30
  hideHeader = false,
31
+ onStartQuiz,
25
32
  }) => {
26
- const { callbacks } = useElearnConfig();
33
+ const { config, callbacks } = useElearnConfig();
27
34
  const theme = useTheme();
28
35
 
36
+ const activeCourse = course || config.courses?.find((c) => c.id === courseId);
37
+ const activeTopic = topic || activeCourse?.topics.find((t) => t.id === topicId) || config.courses?.flatMap((c) => c.topics).find((t) => t.id === topicId);
38
+
29
39
  const handleStart = () => {
30
- if (callbacks.onStartQuiz && topic.questions) {
31
- callbacks.onStartQuiz(topic.id, topic.questions, topic.title);
40
+ if (activeTopic && activeTopic.questions) {
41
+ if (onStartQuiz) {
42
+ onStartQuiz(activeTopic.id, activeTopic.questions, activeTopic.title);
43
+ } else {
44
+ callbacks.onStartQuiz?.(activeTopic.id, activeTopic.questions, activeTopic.title);
45
+ }
32
46
  }
33
47
  };
34
48
 
49
+ if (!activeTopic) {
50
+ return (
51
+ <SafeAreaView style={[styles.safeArea, { backgroundColor: theme.background }]}>
52
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }}>
53
+ <Typography variant="h2" bold style={{ marginBottom: 12 }}>Topic not found</Typography>
54
+ <TouchableOpacity
55
+ onPress={onBack}
56
+ style={{
57
+ paddingHorizontal: 20,
58
+ paddingVertical: 10,
59
+ borderRadius: 8,
60
+ backgroundColor: theme.primary,
61
+ }}
62
+ >
63
+ <Typography variant="body" bold color="#FFFFFF">Go Back</Typography>
64
+ </TouchableOpacity>
65
+ </View>
66
+ </SafeAreaView>
67
+ );
68
+ }
69
+
35
70
  // Mock list of glance topics matching mockup 3
36
71
  const glanceTopics = [
37
72
  'Cerebral Cortex',
@@ -67,7 +102,7 @@ export const QuizStartScreen: React.FC<QuizStartScreenProps> = ({
67
102
 
68
103
  {/* Section Category Title */}
69
104
  <Typography variant="h1" bold color={theme.textPrimary} style={styles.categoryTitle}>
70
- {topic.category || 'Neuro Anatomy'}
105
+ {activeTopic.category || 'Neuro Anatomy'}
71
106
  </Typography>
72
107
 
73
108
  {/* Main Info Box */}
@@ -80,10 +115,10 @@ export const QuizStartScreen: React.FC<QuizStartScreenProps> = ({
80
115
  {/* Right details */}
81
116
  <View style={styles.topicHeaderDetails}>
82
117
  <Typography variant="h2" bold color={theme.textPrimary} style={styles.topicMainTitle}>
83
- {topic.title}
118
+ {activeTopic.title}
84
119
  </Typography>
85
120
  <Typography variant="caption" color={theme.textSecondary}>
86
- {topic.totalQuestions} Questions
121
+ {activeTopic.totalQuestions} Questions
87
122
  </Typography>
88
123
  </View>
89
124
  </View>
@@ -94,7 +129,7 @@ export const QuizStartScreen: React.FC<QuizStartScreenProps> = ({
94
129
  onPress={handleStart}
95
130
  variant="primary"
96
131
  style={[styles.startButton, { backgroundColor: theme.primary }]}
97
- disabled={!topic.questions || topic.questions.length === 0}
132
+ disabled={!activeTopic.questions || activeTopic.questions.length === 0}
98
133
  />
99
134
  </Card>
100
135
 
@@ -15,7 +15,8 @@ import { Typography, Card, CircularProgress, Button, Icon } from '../components'
15
15
 
16
16
  interface ResultScreenProps {
17
17
  results: QuizResults;
18
- questions: Question[];
18
+ questions?: Question[];
19
+ topicId?: string;
19
20
  onClose: () => void;
20
21
  onReviewAnswers?: () => void;
21
22
  hideHeader?: boolean;
@@ -24,12 +25,16 @@ interface ResultScreenProps {
24
25
  export const ResultScreen: React.FC<ResultScreenProps> = ({
25
26
  results,
26
27
  questions,
28
+ topicId,
27
29
  onClose,
28
30
  hideHeader = false,
29
31
  }) => {
30
- const { getString } = useElearnConfig();
32
+ const { config, getString } = useElearnConfig();
31
33
  const theme = useTheme();
32
34
 
35
+ const activeTopic = topicId ? config.courses?.flatMap((c) => c.topics).find((t) => t.id === topicId) : undefined;
36
+ const activeQuestions = questions || activeTopic?.questions || [];
37
+
33
38
  // Selected question index for individual review
34
39
  const [selectedReviewIndex, setSelectedReviewIndex] = useState<number | null>(null);
35
40
 
@@ -51,14 +56,14 @@ export const ResultScreen: React.FC<ResultScreenProps> = ({
51
56
  };
52
57
 
53
58
  const handleNextReview = () => {
54
- if (selectedReviewIndex !== null && selectedReviewIndex < questions.length - 1) {
59
+ if (selectedReviewIndex !== null && selectedReviewIndex < activeQuestions.length - 1) {
55
60
  setSelectedReviewIndex(selectedReviewIndex + 1);
56
61
  }
57
62
  };
58
63
 
59
64
  // --- Sub-View: Individual Question Detail Review ---
60
65
  if (selectedReviewIndex !== null) {
61
- const currentReviewQuestion = questions[selectedReviewIndex]!;
66
+ const currentReviewQuestion = activeQuestions[selectedReviewIndex]!;
62
67
  const userAnswerIdx = results.userAnswers[currentReviewQuestion.id];
63
68
  // Removed unused isCorrect variable
64
69
 
@@ -88,7 +93,7 @@ export const ResultScreen: React.FC<ResultScreenProps> = ({
88
93
  {/* Question Index Meta */}
89
94
  <View style={styles.metaRow}>
90
95
  <Typography variant="caption" bold color={theme.textSecondary} style={styles.questionNumLabel}>
91
- QUESTION {selectedReviewIndex + 1}/{questions.length}
96
+ QUESTION {selectedReviewIndex + 1}/{activeQuestions.length}
92
97
  </Typography>
93
98
  <View style={styles.metaActions}>
94
99
  <TouchableOpacity style={styles.metaActionBtn}>
@@ -211,11 +216,11 @@ export const ResultScreen: React.FC<ResultScreenProps> = ({
211
216
  {/* Next Button */}
212
217
  <TouchableOpacity
213
218
  onPress={handleNextReview}
214
- disabled={selectedReviewIndex === questions.length - 1}
219
+ disabled={selectedReviewIndex === activeQuestions.length - 1}
215
220
  style={[
216
221
  styles.nextActionBtn,
217
222
  { backgroundColor: theme.primary },
218
- selectedReviewIndex === questions.length - 1 && { opacity: 0.4 }
223
+ selectedReviewIndex === activeQuestions.length - 1 && { opacity: 0.4 }
219
224
  ]}
220
225
  >
221
226
  <Typography variant="body" bold color="#FFFFFF">
@@ -303,7 +308,7 @@ export const ResultScreen: React.FC<ResultScreenProps> = ({
303
308
  </View>
304
309
 
305
310
  <View style={styles.reviewList}>
306
- {questions.map((question, idx) => {
311
+ {activeQuestions.map((question, idx) => {
307
312
  const userAnswerIdx = results.userAnswers[question.id];
308
313
  const isCorrect = userAnswerIdx === question.correctOptionIndex;
309
314
 
@@ -13,26 +13,53 @@ import { useTheme } from '../context/ThemeContext';
13
13
  import { Typography, Card, Icon } from '../components';
14
14
 
15
15
  interface TopicListScreenProps {
16
- course: Course;
16
+ course?: Course;
17
+ courseId?: string;
17
18
  onBack: () => void;
18
19
  hideHeader?: boolean;
20
+ onSelectTopic?: (course: Course, topic: Topic) => void;
19
21
  }
20
22
 
21
23
  export const TopicListScreen: React.FC<TopicListScreenProps> = ({
22
24
  course,
25
+ courseId,
23
26
  onBack,
24
27
  hideHeader = false,
28
+ onSelectTopic,
25
29
  }) => {
26
- const { callbacks } = useElearnConfig();
30
+ const { config, callbacks } = useElearnConfig();
27
31
  const theme = useTheme();
28
32
 
33
+ const activeCourse = course || config.courses?.find((c) => c.id === courseId);
34
+
29
35
  // Active filter tab
30
36
  const [activeTab, setActiveTab] = useState('All');
31
37
  const tabs = ['All', 'New', 'Completed', 'Paused', 'Not Started'];
32
38
 
39
+ if (!activeCourse) {
40
+ return (
41
+ <SafeAreaView style={[styles.safeArea, { backgroundColor: theme.background }]}>
42
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 }}>
43
+ <Typography variant="h2" bold style={{ marginBottom: 12 }}>Course not found</Typography>
44
+ <TouchableOpacity
45
+ onPress={onBack}
46
+ style={{
47
+ paddingHorizontal: 20,
48
+ paddingVertical: 10,
49
+ borderRadius: 8,
50
+ backgroundColor: theme.primary,
51
+ }}
52
+ >
53
+ <Typography variant="body" bold color="#FFFFFF">Go Back</Typography>
54
+ </TouchableOpacity>
55
+ </View>
56
+ </SafeAreaView>
57
+ );
58
+ }
59
+
33
60
  // Group topics by category
34
61
  const categoriesMap: { [key: string]: Topic[] } = {};
35
- course.topics.forEach((topic) => {
62
+ activeCourse.topics.forEach((topic) => {
36
63
  const cat = topic.category || 'GENERAL';
37
64
  if (!categoriesMap[cat]) {
38
65
  categoriesMap[cat] = [];
@@ -69,10 +96,10 @@ export const TopicListScreen: React.FC<TopicListScreenProps> = ({
69
96
  {/* Subject Header Suffix */}
70
97
  <View style={styles.subjectHeader}>
71
98
  <Typography variant="h1" bold color={theme.textPrimary} style={styles.subjectTitle}>
72
- {course.title}
99
+ {activeCourse.title}
73
100
  </Typography>
74
101
  <Typography variant="body" color={theme.textSecondary}>
75
- {course.topics.length} Modules
102
+ {activeCourse.topics.length} Modules
76
103
  </Typography>
77
104
  </View>
78
105
 
@@ -122,7 +149,7 @@ export const TopicListScreen: React.FC<TopicListScreenProps> = ({
122
149
  {list.map((topic) => {
123
150
  globalIndex++;
124
151
  const currentIndex = globalIndex;
125
- const isLastInCourse = currentIndex === course.topics.length;
152
+ const isLastInCourse = currentIndex === activeCourse.topics.length;
126
153
 
127
154
  return (
128
155
  <View key={topic.id} style={styles.timelineItemRow}>
@@ -146,7 +173,13 @@ export const TopicListScreen: React.FC<TopicListScreenProps> = ({
146
173
  {/* Right Side Info Card */}
147
174
  <TouchableOpacity
148
175
  activeOpacity={0.8}
149
- onPress={() => callbacks.onSelectTopic?.(course, topic)}
176
+ onPress={() => {
177
+ if (onSelectTopic) {
178
+ onSelectTopic(activeCourse, topic);
179
+ } else {
180
+ callbacks.onSelectTopic?.(activeCourse, topic);
181
+ }
182
+ }}
150
183
  style={styles.cardTouchable}
151
184
  >
152
185
  <Card bordered style={styles.topicCard}>
@@ -4,3 +4,4 @@ export { TopicListScreen } from './TopicListScreen';
4
4
  export { QuizStartScreen } from './QuizStartScreen';
5
5
  export { QuestionScreen } from './QuestionScreen';
6
6
  export { ResultScreen } from './ResultScreen';
7
+ export { PearlsScreen } from './PearlsScreen';