react-native-elearn-ui 0.1.28 → 0.1.32

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 (34) hide show
  1. package/lib/module/components/Icon.js +9 -0
  2. package/lib/module/components/Icon.js.map +1 -1
  3. package/lib/module/index.js +1 -0
  4. package/lib/module/index.js.map +1 -1
  5. package/lib/module/screens/index.js +2 -0
  6. package/lib/module/screens/index.js.map +1 -1
  7. package/lib/module/screens/qbank/QBankOverviewScreen.js +435 -0
  8. package/lib/module/screens/qbank/QBankOverviewScreen.js.map +1 -0
  9. package/lib/module/screens/qbank/QBankQuestionScreen.js +181 -123
  10. package/lib/module/screens/qbank/QBankQuestionScreen.js.map +1 -1
  11. package/lib/module/screens/qbank/QBankSuccessScreen.js +336 -0
  12. package/lib/module/screens/qbank/QBankSuccessScreen.js.map +1 -0
  13. package/lib/typescript/src/components/Icon.d.ts +1 -1
  14. package/lib/typescript/src/components/Icon.d.ts.map +1 -1
  15. package/lib/typescript/src/index.d.ts +1 -0
  16. package/lib/typescript/src/index.d.ts.map +1 -1
  17. package/lib/typescript/src/screens/index.d.ts +2 -0
  18. package/lib/typescript/src/screens/index.d.ts.map +1 -1
  19. package/lib/typescript/src/screens/qbank/QBankOverviewScreen.d.ts +4 -0
  20. package/lib/typescript/src/screens/qbank/QBankOverviewScreen.d.ts.map +1 -0
  21. package/lib/typescript/src/screens/qbank/QBankQuestionScreen.d.ts +7 -9
  22. package/lib/typescript/src/screens/qbank/QBankQuestionScreen.d.ts.map +1 -1
  23. package/lib/typescript/src/screens/qbank/QBankSuccessScreen.d.ts +4 -0
  24. package/lib/typescript/src/screens/qbank/QBankSuccessScreen.d.ts.map +1 -0
  25. package/lib/typescript/src/types/index.d.ts +39 -7
  26. package/lib/typescript/src/types/index.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/components/Icon.tsx +6 -1
  29. package/src/index.tsx +1 -0
  30. package/src/screens/index.ts +2 -0
  31. package/src/screens/qbank/QBankOverviewScreen.tsx +392 -0
  32. package/src/screens/qbank/QBankQuestionScreen.tsx +164 -109
  33. package/src/screens/qbank/QBankSuccessScreen.tsx +292 -0
  34. package/src/types/index.ts +43 -7
@@ -9,18 +9,16 @@ import {
9
9
  import { SafeAreaView } from 'react-native-safe-area-context';
10
10
  import { useTheme } from '../../context/ThemeContext';
11
11
  import { Typography, Icon, Button } from '../../components';
12
- import type { QBankQuestionParams, QBankOption } from '../../types';
12
+ import type { QBankExamData, QBankExamOption } from '../../types';
13
13
 
14
14
  interface QBankQuestionScreenProps {
15
- params: QBankQuestionParams;
16
- initialTimeSeconds?: number;
17
- onTimeUp?: () => void;
15
+ examData: QBankExamData;
16
+ onFinish?: (results: Record<string, string>) => void;
17
+ onQuestionAttempt?: (questionId: string, selectedOptionId: string) => void;
18
18
  onBack?: () => void;
19
- onNext?: () => void;
20
- onPrev?: () => void;
21
- onPressOption?: (optionId: string) => void;
22
- onPressReport?: () => void;
23
- onPressUsers?: () => void;
19
+ onPressReport?: (questionId: string) => void;
20
+ onPressUsers?: (questionId: string) => void;
21
+ isProUser?: boolean;
24
22
  }
25
23
 
26
24
  const formatTime = (totalSeconds: number) => {
@@ -30,22 +28,30 @@ const formatTime = (totalSeconds: number) => {
30
28
  };
31
29
 
32
30
  export const QBankQuestionScreen: React.FC<QBankQuestionScreenProps> = ({
33
- params,
34
- initialTimeSeconds = 60,
35
- onTimeUp,
31
+ examData,
32
+ onFinish,
33
+ onQuestionAttempt,
36
34
  onBack,
37
- onNext,
38
- onPrev,
39
- onPressOption,
40
35
  onPressReport,
41
36
  onPressUsers,
37
+ isProUser = false,
42
38
  }) => {
43
39
  const theme = useTheme();
44
- const [timeLeft, setTimeLeft] = useState(initialTimeSeconds);
40
+ const [currentIndex, setCurrentIndex] = useState(0);
41
+ const [answers, setAnswers] = useState<Record<string, string>>({});
42
+ const [timeLeft, setTimeLeft] = useState(examData.timeLimitSeconds || 0);
43
+
44
+ const currentQuestion = examData.questions[currentIndex];
45
+ const totalQuestions = examData.questions.length;
46
+ const isAttempted = !!currentQuestion && !!answers[currentQuestion.id];
47
+ const selectedOptionId = currentQuestion ? answers[currentQuestion.id] : undefined;
48
+ const isLastQuestion = currentIndex === totalQuestions - 1;
45
49
 
46
50
  useEffect(() => {
51
+ if (!examData.timeLimitSeconds) return;
52
+
47
53
  if (timeLeft <= 0) {
48
- onTimeUp?.();
54
+ onFinish?.(answers);
49
55
  return;
50
56
  }
51
57
 
@@ -54,42 +60,74 @@ export const QBankQuestionScreen: React.FC<QBankQuestionScreenProps> = ({
54
60
  }, 1000);
55
61
 
56
62
  return () => clearInterval(timer);
57
- }, [timeLeft, onTimeUp]);
63
+ }, [timeLeft, examData.timeLimitSeconds, onFinish, answers]);
64
+
65
+ const handleOptionPress = (optionId: string) => {
66
+ if (!currentQuestion || isAttempted) return; // Prevent changing answer in Tutor mode
67
+
68
+ setAnswers((prev) => ({
69
+ ...prev,
70
+ [currentQuestion.id]: optionId,
71
+ }));
72
+
73
+ onQuestionAttempt?.(currentQuestion.id, optionId);
74
+ };
75
+
76
+ const handleNextOrFinish = () => {
77
+ if (isLastQuestion) {
78
+ onFinish?.(answers);
79
+ } else {
80
+ setCurrentIndex((prev) => prev + 1);
81
+ }
82
+ };
83
+
84
+ const handlePrev = () => {
85
+ if (currentIndex > 0) {
86
+ setCurrentIndex((prev) => prev - 1);
87
+ }
88
+ };
58
89
 
59
- const progressPercentage = (params.questionNumber / params.totalQuestions) * 100;
90
+ const progressPercentage = ((currentIndex + 1) / totalQuestions) * 100;
60
91
 
61
- const renderOption = (option: QBankOption, index: number) => {
92
+ const renderOption = (option: QBankExamOption, index: number) => {
62
93
  const letters = ['A', 'B', 'C', 'D', 'E', 'F'];
63
94
  const letter = letters[index] || '';
64
95
 
65
- let borderColor = '#E5E7EB'; // Default grey
96
+ let borderColor = '#E5E7EB';
66
97
  let backgroundColor = '#FFFFFF';
67
98
  let iconName: 'check-circle' | 'x-circle' | undefined;
68
99
  let iconColor = theme.textSecondary;
69
100
  let statusText = '';
70
101
  let statusColor = '';
71
102
 
72
- if (option.status === 'correct') {
73
- borderColor = theme.success;
74
- backgroundColor = '#F0FDF4'; // light green bg
75
- iconName = 'check-circle';
76
- iconColor = theme.success;
77
- statusText = 'Correct';
78
- statusColor = theme.success;
79
- } else if (option.status === 'incorrect') {
80
- borderColor = '#EF4444';
81
- backgroundColor = '#FEF2F2'; // light red bg
82
- iconName = 'x-circle';
83
- iconColor = '#EF4444';
84
- statusText = 'InCorrect'; // matching user image
85
- statusColor = '#EF4444';
103
+ const isSelected = selectedOptionId === option.id;
104
+
105
+ if (isAttempted) {
106
+ if (option.isCorrect) {
107
+ borderColor = theme.success;
108
+ backgroundColor = '#F0FDF4';
109
+ iconName = 'check-circle';
110
+ iconColor = theme.success;
111
+ statusText = 'Correct';
112
+ statusColor = theme.success;
113
+ } else if (isSelected && !option.isCorrect) {
114
+ borderColor = '#EF4444';
115
+ backgroundColor = '#FEF2F2';
116
+ iconName = 'x-circle';
117
+ iconColor = '#EF4444';
118
+ statusText = 'InCorrect';
119
+ statusColor = '#EF4444';
120
+ }
121
+ } else if (isSelected) {
122
+ // Very brief state before it evaluates (if we ever want a delay), but here it evaluates instantly
123
+ borderColor = theme.primary;
86
124
  }
87
125
 
88
126
  return (
89
127
  <TouchableOpacity
90
128
  key={option.id}
91
129
  style={[styles.optionContainer, { borderColor, backgroundColor }]}
92
- onPress={() => onPressOption?.(option.id)}
130
+ onPress={() => handleOptionPress(option.id)}
93
131
  activeOpacity={0.7}
94
132
  >
95
133
  <View style={styles.optionContent}>
@@ -102,12 +140,12 @@ export const QBankQuestionScreen: React.FC<QBankQuestionScreenProps> = ({
102
140
  <Typography variant="body" color={theme.textPrimary} style={styles.optionText}>
103
141
  {letter}. {option.text}
104
142
  </Typography>
105
- {(option.status === 'correct' || option.status === 'incorrect') && (
143
+ {isAttempted && statusText ? (
106
144
  <Typography variant="caption" color={statusColor} style={styles.optionStatusText}>
107
- {statusText} <Typography variant="caption" color={theme.textSecondary}>[{option.percentage}%]</Typography>
145
+ {statusText} {option.percentage !== undefined && <Typography variant="caption" color={theme.textSecondary}>[{option.percentage}%]</Typography>}
108
146
  </Typography>
109
- )}
110
- {option.status === 'default' && option.percentage !== undefined && (
147
+ ) : null}
148
+ {!statusText && option.percentage !== undefined && isAttempted && (
111
149
  <Typography variant="caption" color={theme.textSecondary} style={styles.optionStatusText}>
112
150
  [{option.percentage}%]
113
151
  </Typography>
@@ -118,27 +156,44 @@ export const QBankQuestionScreen: React.FC<QBankQuestionScreenProps> = ({
118
156
  );
119
157
  };
120
158
 
159
+ if (!currentQuestion) return null;
160
+
121
161
  return (
122
- <SafeAreaView style={[styles.container, { backgroundColor: '#E5E7EB' }]} edges={['top', 'bottom']}>
162
+ <SafeAreaView style={[styles.container, { backgroundColor: '#FFFFFF' }]} edges={['top', 'bottom']}>
123
163
  <StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
124
164
 
125
- <View style={styles.card}>
126
- {/* Header */}
127
- <View style={styles.header}>
128
- <TouchableOpacity onPress={onBack} style={styles.headerIconBtnLeft}>
129
- <Icon name="chevron-left" size={24} color={theme.textPrimary} />
130
- </TouchableOpacity>
131
- <Typography variant="h3" color={theme.textPrimary} style={styles.headerTitle}>
132
- {params.headerTitle}
133
- </Typography>
134
- <View style={styles.headerIconBtnRight} />
165
+ {/* Header */}
166
+ <View style={styles.header}>
167
+ <TouchableOpacity onPress={onBack} style={styles.headerIconBtnLeft}>
168
+ <Icon name="chevron-left" size={24} color={theme.textPrimary} />
169
+ </TouchableOpacity>
170
+ <Typography variant="h3" color={theme.textPrimary} style={styles.headerTitle}>
171
+ {examData.title}
172
+ </Typography>
173
+ <View style={styles.headerIconBtnRight}>
174
+ {examData.accessType === 'pro' && !isProUser && (
175
+ <View style={styles.proBadge}>
176
+ <Typography variant="caption" bold color="#FFFFFF" style={{fontSize: 8}}>
177
+ PRO
178
+ </Typography>
179
+ </View>
180
+ )}
181
+ {examData.accessType === 'free' && (
182
+ <View style={styles.freeBadge}>
183
+ <Typography variant="caption" bold color={theme.success} style={{fontSize: 8}}>
184
+ FREE
185
+ </Typography>
186
+ </View>
187
+ )}
135
188
  </View>
189
+ </View>
136
190
 
137
- {/* Progress Bar */}
138
- <View style={styles.progressTrack}>
139
- <View style={[styles.progressFill, { width: `${progressPercentage}%`, backgroundColor: theme.success }]} />
140
- </View>
191
+ {/* Progress Bar */}
192
+ <View style={styles.progressTrack}>
193
+ <View style={[styles.progressFill, { width: `${progressPercentage}%`, backgroundColor: theme.success }]} />
194
+ </View>
141
195
 
196
+ <View style={{ flex: 1, backgroundColor: '#F9FAFB' }}>
142
197
  <ScrollView
143
198
  contentContainerStyle={styles.scrollContent}
144
199
  showsVerticalScrollIndicator={false}
@@ -146,73 +201,75 @@ export const QBankQuestionScreen: React.FC<QBankQuestionScreenProps> = ({
146
201
  {/* Top Bar: Question Number & Timer */}
147
202
  <View style={styles.topBar}>
148
203
  <Typography variant="caption" bold color={theme.textSecondary} style={styles.questionNumber}>
149
- QUESTION {params.questionNumber}/{params.totalQuestions}
204
+ QUESTION {currentIndex + 1}/{totalQuestions}
150
205
  </Typography>
151
- <View style={styles.timerWrapper}>
152
- <Icon name="clock" size={14} color={theme.textSecondary} />
153
- <Typography variant="caption" color={theme.textSecondary} style={styles.timerText}>
154
- {formatTime(timeLeft)}
155
- </Typography>
156
- </View>
206
+ {!!examData.timeLimitSeconds && (
207
+ <View style={styles.timerWrapper}>
208
+ <Icon name="clock" size={14} color={theme.textSecondary} />
209
+ <Typography variant="caption" color={theme.textSecondary} style={styles.timerText}>
210
+ {formatTime(timeLeft)}
211
+ </Typography>
212
+ </View>
213
+ )}
157
214
  </View>
158
215
 
159
216
  {/* Question Text */}
160
217
  <Typography variant="h3" bold color={theme.textPrimary} style={styles.questionText}>
161
- {params.questionText}
218
+ {currentQuestion.questionText}
162
219
  </Typography>
163
220
 
164
221
  {/* Options */}
165
222
  <View style={styles.optionsList}>
166
- {params.options.map((option, index) => renderOption(option, index))}
223
+ {currentQuestion.options.map((option, index) => renderOption(option, index))}
167
224
  </View>
168
225
 
169
- {/* Explanation Section */}
170
- {params.explanation && (
226
+ {/* Explanation Section (Only shown if attempted) */}
227
+ {isAttempted && currentQuestion.explanation && (
171
228
  <View style={styles.explanationContainer}>
172
229
  <Typography variant="caption" bold color={theme.textPrimary} style={styles.explanationTitle}>
173
230
  EXPLANATION
174
231
  </Typography>
175
232
  <Typography variant="body" color={theme.textSecondary} style={styles.explanationText}>
176
- {params.explanation}
233
+ {currentQuestion.explanation}
177
234
  </Typography>
178
235
 
179
- {params.referenceId && (
236
+ {currentQuestion.referenceId && (
180
237
  <View style={styles.referenceContainer}>
181
238
  <Typography variant="caption" color={theme.textSecondary} style={styles.referenceText}>
182
- {params.referenceId}
239
+ {currentQuestion.referenceId}
183
240
  </Typography>
184
241
  </View>
185
242
  )}
186
243
  </View>
187
244
  )}
188
245
  </ScrollView>
246
+ </View>
189
247
 
190
- {/* Bottom Actions */}
191
- <View style={styles.bottomActions}>
192
- <View style={styles.leftActions}>
193
- <TouchableOpacity style={styles.iconAction} onPress={onPressReport}>
194
- <Icon name="alert-circle" size={20} color={theme.textSecondary} />
195
- </TouchableOpacity>
196
- <TouchableOpacity style={styles.iconAction} onPress={onPressUsers}>
197
- <Icon name="users" size={20} color={theme.textSecondary} />
198
- </TouchableOpacity>
199
- </View>
200
- <View style={styles.rightActions}>
201
- <TouchableOpacity style={styles.prevBtn} onPress={onPrev}>
202
- <Typography variant="body" bold color={theme.textPrimary}>
203
- Prev
204
- </Typography>
205
- </TouchableOpacity>
206
- <Button
207
- title="Next"
208
- onPress={onNext || (() => {})}
209
- style={styles.nextBtn}
210
- textStyle={styles.nextBtnText}
211
- />
212
- </View>
248
+ {/* Bottom Actions */}
249
+ <View style={styles.bottomActions}>
250
+ <View style={styles.leftActions}>
251
+ <TouchableOpacity style={styles.iconAction} onPress={() => onPressReport?.(currentQuestion.id)}>
252
+ <Icon name="alert-circle" size={20} color={theme.textSecondary} />
253
+ </TouchableOpacity>
254
+ <TouchableOpacity style={styles.iconAction} onPress={() => onPressUsers?.(currentQuestion.id)}>
255
+ <Icon name="users" size={20} color={theme.textSecondary} />
256
+ </TouchableOpacity>
257
+ </View>
258
+ <View style={styles.rightActions}>
259
+ <TouchableOpacity style={[styles.prevBtn, currentIndex === 0 && { opacity: 0.5 }]} onPress={handlePrev} disabled={currentIndex === 0}>
260
+ <Typography variant="body" bold color={theme.textPrimary}>
261
+ Prev
262
+ </Typography>
263
+ </TouchableOpacity>
264
+ <Button
265
+ title={isLastQuestion ? "Finish" : "Next"}
266
+ onPress={handleNextOrFinish}
267
+ style={styles.nextBtn}
268
+ textStyle={styles.nextBtnText}
269
+ />
213
270
  </View>
214
-
215
271
  </View>
272
+
216
273
  </SafeAreaView>
217
274
  );
218
275
  };
@@ -220,18 +277,6 @@ export const QBankQuestionScreen: React.FC<QBankQuestionScreenProps> = ({
220
277
  const styles = StyleSheet.create({
221
278
  container: {
222
279
  flex: 1,
223
- padding: 16, // Gray margin around the card
224
- },
225
- card: {
226
- flex: 1,
227
- backgroundColor: '#FFFFFF',
228
- borderRadius: 8,
229
- overflow: 'hidden',
230
- shadowColor: '#000',
231
- shadowOffset: { width: 0, height: 2 },
232
- shadowOpacity: 0.1,
233
- shadowRadius: 4,
234
- elevation: 4,
235
280
  },
236
281
  header: {
237
282
  flexDirection: 'row',
@@ -245,10 +290,6 @@ const styles = StyleSheet.create({
245
290
  justifyContent: 'center',
246
291
  alignItems: 'flex-start',
247
292
  },
248
- headerRightActions: {
249
- flexDirection: 'row',
250
- alignItems: 'center',
251
- },
252
293
  headerIconBtnRight: {
253
294
  width: 40,
254
295
  height: 40,
@@ -261,6 +302,19 @@ const styles = StyleSheet.create({
261
302
  flex: 1,
262
303
  textAlign: 'center',
263
304
  },
305
+ proBadge: {
306
+ paddingHorizontal: 6,
307
+ paddingVertical: 2,
308
+ borderRadius: 4,
309
+ backgroundColor: '#F59E0B',
310
+ },
311
+ freeBadge: {
312
+ paddingHorizontal: 6,
313
+ paddingVertical: 2,
314
+ borderRadius: 4,
315
+ borderWidth: 1,
316
+ borderColor: '#10B981',
317
+ },
264
318
  progressTrack: {
265
319
  height: 4,
266
320
  backgroundColor: '#E5E7EB',
@@ -304,6 +358,7 @@ const styles = StyleSheet.create({
304
358
  borderRadius: 8,
305
359
  padding: 16,
306
360
  marginBottom: 12,
361
+ backgroundColor: '#FFFFFF',
307
362
  },
308
363
  optionContent: {
309
364
  flexDirection: 'row',