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.
- package/lib/module/components/Icon.js +9 -0
- package/lib/module/components/Icon.js.map +1 -1
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/screens/index.js +2 -0
- package/lib/module/screens/index.js.map +1 -1
- package/lib/module/screens/qbank/QBankOverviewScreen.js +435 -0
- package/lib/module/screens/qbank/QBankOverviewScreen.js.map +1 -0
- package/lib/module/screens/qbank/QBankQuestionScreen.js +181 -123
- package/lib/module/screens/qbank/QBankQuestionScreen.js.map +1 -1
- package/lib/module/screens/qbank/QBankSuccessScreen.js +336 -0
- package/lib/module/screens/qbank/QBankSuccessScreen.js.map +1 -0
- package/lib/typescript/src/components/Icon.d.ts +1 -1
- package/lib/typescript/src/components/Icon.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/index.d.ts +2 -0
- package/lib/typescript/src/screens/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/qbank/QBankOverviewScreen.d.ts +4 -0
- package/lib/typescript/src/screens/qbank/QBankOverviewScreen.d.ts.map +1 -0
- package/lib/typescript/src/screens/qbank/QBankQuestionScreen.d.ts +7 -9
- package/lib/typescript/src/screens/qbank/QBankQuestionScreen.d.ts.map +1 -1
- package/lib/typescript/src/screens/qbank/QBankSuccessScreen.d.ts +4 -0
- package/lib/typescript/src/screens/qbank/QBankSuccessScreen.d.ts.map +1 -0
- package/lib/typescript/src/types/index.d.ts +39 -7
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/Icon.tsx +6 -1
- package/src/index.tsx +1 -0
- package/src/screens/index.ts +2 -0
- package/src/screens/qbank/QBankOverviewScreen.tsx +392 -0
- package/src/screens/qbank/QBankQuestionScreen.tsx +164 -109
- package/src/screens/qbank/QBankSuccessScreen.tsx +292 -0
- 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 {
|
|
12
|
+
import type { QBankExamData, QBankExamOption } from '../../types';
|
|
13
13
|
|
|
14
14
|
interface QBankQuestionScreenProps {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
examData: QBankExamData;
|
|
16
|
+
onFinish?: (results: Record<string, string>) => void;
|
|
17
|
+
onQuestionAttempt?: (questionId: string, selectedOptionId: string) => void;
|
|
18
18
|
onBack?: () => void;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 [
|
|
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
|
-
|
|
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,
|
|
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 = (
|
|
90
|
+
const progressPercentage = ((currentIndex + 1) / totalQuestions) * 100;
|
|
60
91
|
|
|
61
|
-
const renderOption = (option:
|
|
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';
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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={() =>
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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: '#
|
|
162
|
+
<SafeAreaView style={[styles.container, { backgroundColor: '#FFFFFF' }]} edges={['top', 'bottom']}>
|
|
123
163
|
<StatusBar barStyle="dark-content" backgroundColor="#FFFFFF" />
|
|
124
164
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
<
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 {
|
|
204
|
+
QUESTION {currentIndex + 1}/{totalQuestions}
|
|
150
205
|
</Typography>
|
|
151
|
-
|
|
152
|
-
<
|
|
153
|
-
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
{
|
|
218
|
+
{currentQuestion.questionText}
|
|
162
219
|
</Typography>
|
|
163
220
|
|
|
164
221
|
{/* Options */}
|
|
165
222
|
<View style={styles.optionsList}>
|
|
166
|
-
{
|
|
223
|
+
{currentQuestion.options.map((option, index) => renderOption(option, index))}
|
|
167
224
|
</View>
|
|
168
225
|
|
|
169
|
-
{/* Explanation Section */}
|
|
170
|
-
{
|
|
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
|
-
{
|
|
233
|
+
{currentQuestion.explanation}
|
|
177
234
|
</Typography>
|
|
178
235
|
|
|
179
|
-
{
|
|
236
|
+
{currentQuestion.referenceId && (
|
|
180
237
|
<View style={styles.referenceContainer}>
|
|
181
238
|
<Typography variant="caption" color={theme.textSecondary} style={styles.referenceText}>
|
|
182
|
-
{
|
|
239
|
+
{currentQuestion.referenceId}
|
|
183
240
|
</Typography>
|
|
184
241
|
</View>
|
|
185
242
|
)}
|
|
186
243
|
</View>
|
|
187
244
|
)}
|
|
188
245
|
</ScrollView>
|
|
246
|
+
</View>
|
|
189
247
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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',
|