react-native-elearn-ui 0.1.25 → 0.1.31

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 (29) hide show
  1. package/lib/module/components/Icon.js +30 -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/index.js +1 -0
  6. package/lib/module/screens/index.js.map +1 -1
  7. package/lib/module/screens/qbank/QBankQuestionScreen.js +473 -0
  8. package/lib/module/screens/qbank/QBankQuestionScreen.js.map +1 -0
  9. package/lib/module/screens/qbank/QBankSubTopicScreen.js +20 -17
  10. package/lib/module/screens/qbank/QBankSubTopicScreen.js.map +1 -1
  11. package/lib/typescript/src/components/Icon.d.ts +1 -1
  12. package/lib/typescript/src/components/Icon.d.ts.map +1 -1
  13. package/lib/typescript/src/index.d.ts +1 -1
  14. package/lib/typescript/src/index.d.ts.map +1 -1
  15. package/lib/typescript/src/screens/index.d.ts +1 -0
  16. package/lib/typescript/src/screens/index.d.ts.map +1 -1
  17. package/lib/typescript/src/screens/qbank/QBankQuestionScreen.d.ts +14 -0
  18. package/lib/typescript/src/screens/qbank/QBankQuestionScreen.d.ts.map +1 -0
  19. package/lib/typescript/src/screens/qbank/QBankSubTopicScreen.d.ts +1 -0
  20. package/lib/typescript/src/screens/qbank/QBankSubTopicScreen.d.ts.map +1 -1
  21. package/lib/typescript/src/types/index.d.ts +20 -0
  22. package/lib/typescript/src/types/index.d.ts.map +1 -1
  23. package/package.json +1 -1
  24. package/src/components/Icon.tsx +12 -2
  25. package/src/index.tsx +1 -0
  26. package/src/screens/index.ts +1 -0
  27. package/src/screens/qbank/QBankQuestionScreen.tsx +448 -0
  28. package/src/screens/qbank/QBankSubTopicScreen.tsx +15 -13
  29. package/src/types/index.ts +23 -0
@@ -0,0 +1,448 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import {
3
+ View,
4
+ ScrollView,
5
+ StyleSheet,
6
+ TouchableOpacity,
7
+ StatusBar,
8
+ } from 'react-native';
9
+ import { SafeAreaView } from 'react-native-safe-area-context';
10
+ import { useTheme } from '../../context/ThemeContext';
11
+ import { Typography, Icon, Button } from '../../components';
12
+ import type { QBankExamData, QBankExamOption } from '../../types';
13
+
14
+ interface QBankQuestionScreenProps {
15
+ examData: QBankExamData;
16
+ onFinish?: (results: Record<string, string>) => void;
17
+ onQuestionAttempt?: (questionId: string, selectedOptionId: string) => void;
18
+ onBack?: () => void;
19
+ onPressReport?: (questionId: string) => void;
20
+ onPressUsers?: (questionId: string) => void;
21
+ isProUser?: boolean;
22
+ }
23
+
24
+ const formatTime = (totalSeconds: number) => {
25
+ const minutes = Math.floor(totalSeconds / 60);
26
+ const seconds = totalSeconds % 60;
27
+ return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
28
+ };
29
+
30
+ export const QBankQuestionScreen: React.FC<QBankQuestionScreenProps> = ({
31
+ examData,
32
+ onFinish,
33
+ onQuestionAttempt,
34
+ onBack,
35
+ onPressReport,
36
+ onPressUsers,
37
+ isProUser = false,
38
+ }) => {
39
+ const theme = useTheme();
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;
49
+
50
+ useEffect(() => {
51
+ if (!examData.timeLimitSeconds) return;
52
+
53
+ if (timeLeft <= 0) {
54
+ onFinish?.(answers);
55
+ return;
56
+ }
57
+
58
+ const timer = setInterval(() => {
59
+ setTimeLeft((prev) => prev - 1);
60
+ }, 1000);
61
+
62
+ return () => clearInterval(timer);
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
+ };
89
+
90
+ const progressPercentage = ((currentIndex + 1) / totalQuestions) * 100;
91
+
92
+ const renderOption = (option: QBankExamOption, index: number) => {
93
+ const letters = ['A', 'B', 'C', 'D', 'E', 'F'];
94
+ const letter = letters[index] || '';
95
+
96
+ let borderColor = '#E5E7EB';
97
+ let backgroundColor = '#FFFFFF';
98
+ let iconName: 'check-circle' | 'x-circle' | undefined;
99
+ let iconColor = theme.textSecondary;
100
+ let statusText = '';
101
+ let statusColor = '';
102
+
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;
124
+ }
125
+
126
+ return (
127
+ <TouchableOpacity
128
+ key={option.id}
129
+ style={[styles.optionContainer, { borderColor, backgroundColor }]}
130
+ onPress={() => handleOptionPress(option.id)}
131
+ activeOpacity={0.7}
132
+ >
133
+ <View style={styles.optionContent}>
134
+ {iconName ? (
135
+ <Icon name={iconName} size={20} color={iconColor} />
136
+ ) : (
137
+ <View style={styles.emptyCircle} />
138
+ )}
139
+ <View style={styles.optionTextWrapper}>
140
+ <Typography variant="body" color={theme.textPrimary} style={styles.optionText}>
141
+ {letter}. {option.text}
142
+ </Typography>
143
+ {isAttempted && statusText ? (
144
+ <Typography variant="caption" color={statusColor} style={styles.optionStatusText}>
145
+ {statusText} {option.percentage !== undefined && <Typography variant="caption" color={theme.textSecondary}>[{option.percentage}%]</Typography>}
146
+ </Typography>
147
+ ) : null}
148
+ {!statusText && option.percentage !== undefined && isAttempted && (
149
+ <Typography variant="caption" color={theme.textSecondary} style={styles.optionStatusText}>
150
+ [{option.percentage}%]
151
+ </Typography>
152
+ )}
153
+ </View>
154
+ </View>
155
+ </TouchableOpacity>
156
+ );
157
+ };
158
+
159
+ if (!currentQuestion) return null;
160
+
161
+ return (
162
+ <SafeAreaView style={[styles.container, { backgroundColor: '#FFFFFF' }]} edges={['top', 'bottom']}>
163
+ <StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
164
+
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
+ )}
188
+ </View>
189
+ </View>
190
+
191
+ {/* Progress Bar */}
192
+ <View style={styles.progressTrack}>
193
+ <View style={[styles.progressFill, { width: `${progressPercentage}%`, backgroundColor: theme.success }]} />
194
+ </View>
195
+
196
+ <View style={{ flex: 1, backgroundColor: '#F9FAFB' }}>
197
+ <ScrollView
198
+ contentContainerStyle={styles.scrollContent}
199
+ showsVerticalScrollIndicator={false}
200
+ >
201
+ {/* Top Bar: Question Number & Timer */}
202
+ <View style={styles.topBar}>
203
+ <Typography variant="caption" bold color={theme.textSecondary} style={styles.questionNumber}>
204
+ QUESTION {currentIndex + 1}/{totalQuestions}
205
+ </Typography>
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
+ )}
214
+ </View>
215
+
216
+ {/* Question Text */}
217
+ <Typography variant="h3" bold color={theme.textPrimary} style={styles.questionText}>
218
+ {currentQuestion.questionText}
219
+ </Typography>
220
+
221
+ {/* Options */}
222
+ <View style={styles.optionsList}>
223
+ {currentQuestion.options.map((option, index) => renderOption(option, index))}
224
+ </View>
225
+
226
+ {/* Explanation Section (Only shown if attempted) */}
227
+ {isAttempted && currentQuestion.explanation && (
228
+ <View style={styles.explanationContainer}>
229
+ <Typography variant="caption" bold color={theme.textPrimary} style={styles.explanationTitle}>
230
+ EXPLANATION
231
+ </Typography>
232
+ <Typography variant="body" color={theme.textSecondary} style={styles.explanationText}>
233
+ {currentQuestion.explanation}
234
+ </Typography>
235
+
236
+ {currentQuestion.referenceId && (
237
+ <View style={styles.referenceContainer}>
238
+ <Typography variant="caption" color={theme.textSecondary} style={styles.referenceText}>
239
+ {currentQuestion.referenceId}
240
+ </Typography>
241
+ </View>
242
+ )}
243
+ </View>
244
+ )}
245
+ </ScrollView>
246
+ </View>
247
+
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
+ />
270
+ </View>
271
+ </View>
272
+
273
+ </SafeAreaView>
274
+ );
275
+ };
276
+
277
+ const styles = StyleSheet.create({
278
+ container: {
279
+ flex: 1,
280
+ },
281
+ header: {
282
+ flexDirection: 'row',
283
+ alignItems: 'center',
284
+ paddingHorizontal: 16,
285
+ paddingVertical: 12,
286
+ },
287
+ headerIconBtnLeft: {
288
+ width: 40,
289
+ height: 40,
290
+ justifyContent: 'center',
291
+ alignItems: 'flex-start',
292
+ },
293
+ headerIconBtnRight: {
294
+ width: 40,
295
+ height: 40,
296
+ justifyContent: 'center',
297
+ alignItems: 'flex-end',
298
+ },
299
+ headerTitle: {
300
+ fontSize: 16,
301
+ fontWeight: '600',
302
+ flex: 1,
303
+ textAlign: 'center',
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
+ },
318
+ progressTrack: {
319
+ height: 4,
320
+ backgroundColor: '#E5E7EB',
321
+ width: '100%',
322
+ },
323
+ progressFill: {
324
+ height: '100%',
325
+ },
326
+ scrollContent: {
327
+ padding: 20,
328
+ paddingBottom: 40,
329
+ },
330
+ topBar: {
331
+ flexDirection: 'row',
332
+ justifyContent: 'space-between',
333
+ alignItems: 'center',
334
+ marginBottom: 20,
335
+ },
336
+ questionNumber: {
337
+ fontSize: 12,
338
+ letterSpacing: 0.5,
339
+ },
340
+ timerWrapper: {
341
+ flexDirection: 'row',
342
+ alignItems: 'center',
343
+ },
344
+ timerText: {
345
+ fontSize: 12,
346
+ marginLeft: 6,
347
+ },
348
+ questionText: {
349
+ fontSize: 16,
350
+ lineHeight: 24,
351
+ marginBottom: 24,
352
+ },
353
+ optionsList: {
354
+ marginBottom: 32,
355
+ },
356
+ optionContainer: {
357
+ borderWidth: 1,
358
+ borderRadius: 8,
359
+ padding: 16,
360
+ marginBottom: 12,
361
+ backgroundColor: '#FFFFFF',
362
+ },
363
+ optionContent: {
364
+ flexDirection: 'row',
365
+ alignItems: 'flex-start',
366
+ },
367
+ emptyCircle: {
368
+ width: 20,
369
+ height: 20,
370
+ borderRadius: 10,
371
+ borderWidth: 1,
372
+ borderColor: '#D1D5DB',
373
+ },
374
+ optionTextWrapper: {
375
+ flex: 1,
376
+ marginLeft: 12,
377
+ },
378
+ optionText: {
379
+ fontSize: 14,
380
+ lineHeight: 20,
381
+ },
382
+ optionStatusText: {
383
+ marginTop: 6,
384
+ fontSize: 12,
385
+ },
386
+ explanationContainer: {
387
+ marginTop: 16,
388
+ },
389
+ explanationTitle: {
390
+ fontSize: 12,
391
+ marginBottom: 12,
392
+ },
393
+ explanationText: {
394
+ fontSize: 14,
395
+ lineHeight: 22,
396
+ },
397
+ referenceContainer: {
398
+ marginTop: 24,
399
+ paddingTop: 16,
400
+ borderTopWidth: 1,
401
+ borderTopColor: '#F3F4F6',
402
+ alignItems: 'center',
403
+ },
404
+ referenceText: {
405
+ fontSize: 12,
406
+ },
407
+ bottomActions: {
408
+ flexDirection: 'row',
409
+ justifyContent: 'space-between',
410
+ alignItems: 'center',
411
+ padding: 16,
412
+ borderTopWidth: 1,
413
+ borderTopColor: '#F3F4F6',
414
+ backgroundColor: '#FFFFFF',
415
+ },
416
+ leftActions: {
417
+ flexDirection: 'row',
418
+ alignItems: 'center',
419
+ },
420
+ iconAction: {
421
+ width: 36,
422
+ height: 36,
423
+ justifyContent: 'center',
424
+ alignItems: 'center',
425
+ marginRight: 8,
426
+ borderRadius: 18,
427
+ borderWidth: 1,
428
+ borderColor: '#E5E7EB',
429
+ },
430
+ rightActions: {
431
+ flexDirection: 'row',
432
+ alignItems: 'center',
433
+ },
434
+ prevBtn: {
435
+ marginRight: 20,
436
+ },
437
+ nextBtn: {
438
+ backgroundColor: '#10B981',
439
+ paddingHorizontal: 24,
440
+ paddingVertical: 10,
441
+ height: 'auto',
442
+ minWidth: 80,
443
+ },
444
+ nextBtnText: {
445
+ fontSize: 14,
446
+ fontWeight: '600',
447
+ },
448
+ });
@@ -19,6 +19,7 @@ interface QBankSubTopicScreenProps {
19
19
  onPressModule?: (moduleId: string) => void;
20
20
  onPressFilter?: (filter: string) => void;
21
21
  onPressSearch?: () => void;
22
+ isProUser?: boolean;
22
23
  }
23
24
 
24
25
  export const QBankSubTopicScreen: React.FC<QBankSubTopicScreenProps> = ({
@@ -28,6 +29,7 @@ export const QBankSubTopicScreen: React.FC<QBankSubTopicScreenProps> = ({
28
29
  onPressModule,
29
30
  onPressFilter,
30
31
  onPressSearch,
32
+ isProUser = false,
31
33
  }) => {
32
34
  const theme = useTheme();
33
35
  const [activeFilter, setActiveFilter] = useState(params.activeFilter || params.filters[0]);
@@ -191,25 +193,27 @@ export const QBankSubTopicScreen: React.FC<QBankSubTopicScreenProps> = ({
191
193
  </Typography>
192
194
  </View>
193
195
  )}
194
- {moduleItem.status === 'locked' && (
195
- <View style={styles.lockedBadge}>
196
- <Icon name="lock" size={12} color="#64748B" />
196
+ {moduleItem.status === 'locked' && !isProUser && (
197
+ <View style={styles.proBadge}>
198
+ <Typography variant="caption" bold color="#FFFFFF" style={{fontSize: 7}}>
199
+ PRO
200
+ </Typography>
197
201
  </View>
198
202
  )}
199
203
  {moduleItem.attemptStatus === 'resume' && (
200
- <Typography variant="caption" bold color="#F59E0B" style={{fontSize: 11}}>
204
+ <Typography variant="caption" bold color="#F59E0B" style={{fontSize: 9}}>
201
205
  RESUME
202
206
  </Typography>
203
207
  )}
204
208
  {moduleItem.attemptStatus === 'completed' && (
205
209
  <View style={{ alignItems: 'flex-end' }}>
206
210
  <View style={styles.completedBadge}>
207
- <Typography variant="caption" bold color="#FFFFFF" style={{fontSize: 8}}>
211
+ <Typography variant="caption" bold color="#FFFFFF" style={{fontSize: 7}}>
208
212
  COMPLETED
209
213
  </Typography>
210
214
  </View>
211
215
  {moduleItem.correctPercentage !== undefined && (
212
- <Typography variant="caption" bold color={theme.success} style={{fontSize: 12, marginTop: 4}}>
216
+ <Typography variant="caption" bold color={theme.success} style={{fontSize: 10, marginTop: 4}}>
213
217
  {moduleItem.correctPercentage}% Correct
214
218
  </Typography>
215
219
  )}
@@ -427,13 +431,11 @@ const styles = StyleSheet.create({
427
431
  borderColor: '#10B981',
428
432
  backgroundColor: '#FFFFFF',
429
433
  },
430
- lockedBadge: {
431
- width: 24,
432
- height: 24,
433
- borderRadius: 12,
434
- backgroundColor: '#F1F5F9',
435
- justifyContent: 'center',
436
- alignItems: 'center',
434
+ proBadge: {
435
+ paddingHorizontal: 6,
436
+ paddingVertical: 2,
437
+ borderRadius: 4,
438
+ backgroundColor: '#F59E0B',
437
439
  },
438
440
  completedBadge: {
439
441
  paddingHorizontal: 6,
@@ -239,6 +239,29 @@ export interface QBankSubTopicSection {
239
239
  modules: QBankSubTopicModule[];
240
240
  }
241
241
 
242
+ export interface QBankExamOption {
243
+ id: string;
244
+ text: string;
245
+ isCorrect: boolean;
246
+ percentage?: number; // e.g. 20 for [20%]
247
+ }
248
+
249
+ export interface QBankExamQuestion {
250
+ id: string;
251
+ questionText: string;
252
+ options: QBankExamOption[];
253
+ explanation?: string;
254
+ referenceId?: string;
255
+ }
256
+
257
+ export interface QBankExamData {
258
+ id: string;
259
+ title: string;
260
+ accessType: 'free' | 'pro';
261
+ timeLimitSeconds?: number;
262
+ questions: QBankExamQuestion[];
263
+ }
264
+
242
265
  export interface QBankSubTopicParams {
243
266
  header: {
244
267
  title: string;