react-native-elearn-ui 0.1.8 → 0.1.10
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 +27 -0
- package/lib/module/components/Icon.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/screens/PearlChaptersScreen.js +357 -0
- package/lib/module/screens/PearlChaptersScreen.js.map +1 -0
- package/lib/module/screens/PearlReaderScreen.js +311 -0
- package/lib/module/screens/PearlReaderScreen.js.map +1 -0
- package/lib/module/screens/PearlsScreen.js +151 -0
- package/lib/module/screens/PearlsScreen.js.map +1 -0
- package/lib/module/screens/QbankScreen.js +9 -2
- package/lib/module/screens/QbankScreen.js.map +1 -1
- package/lib/module/screens/QuestionScreen.js +22 -10
- package/lib/module/screens/QuestionScreen.js.map +1 -1
- package/lib/module/screens/QuizStartScreen.js +55 -7
- package/lib/module/screens/QuizStartScreen.js.map +1 -1
- package/lib/module/screens/ResultScreen.js +10 -6
- package/lib/module/screens/ResultScreen.js.map +1 -1
- package/lib/module/screens/TopicListScreen.js +53 -6
- package/lib/module/screens/TopicListScreen.js.map +1 -1
- package/lib/module/screens/index.js +3 -0
- package/lib/module/screens/index.js.map +1 -1
- package/lib/module/screens/pearlsMockData.js +77 -0
- package/lib/module/screens/pearlsMockData.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 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/PearlChaptersScreen.d.ts +12 -0
- package/lib/typescript/src/screens/PearlChaptersScreen.d.ts.map +1 -0
- package/lib/typescript/src/screens/PearlReaderScreen.d.ts +13 -0
- package/lib/typescript/src/screens/PearlReaderScreen.d.ts.map +1 -0
- package/lib/typescript/src/screens/PearlsScreen.d.ts +10 -0
- package/lib/typescript/src/screens/PearlsScreen.d.ts.map +1 -0
- package/lib/typescript/src/screens/QbankScreen.d.ts +2 -0
- package/lib/typescript/src/screens/QbankScreen.d.ts.map +1 -1
- package/lib/typescript/src/screens/QuestionScreen.d.ts +6 -4
- package/lib/typescript/src/screens/QuestionScreen.d.ts.map +1 -1
- package/lib/typescript/src/screens/QuizStartScreen.d.ts +6 -3
- package/lib/typescript/src/screens/QuizStartScreen.d.ts.map +1 -1
- package/lib/typescript/src/screens/ResultScreen.d.ts +2 -1
- package/lib/typescript/src/screens/ResultScreen.d.ts.map +1 -1
- package/lib/typescript/src/screens/TopicListScreen.d.ts +4 -2
- package/lib/typescript/src/screens/TopicListScreen.d.ts.map +1 -1
- package/lib/typescript/src/screens/index.d.ts +3 -0
- package/lib/typescript/src/screens/index.d.ts.map +1 -1
- package/lib/typescript/src/screens/pearlsMockData.d.ts +3 -0
- package/lib/typescript/src/screens/pearlsMockData.d.ts.map +1 -0
- package/lib/typescript/src/types/index.d.ts +15 -0
- package/lib/typescript/src/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/Icon.tsx +35 -1
- package/src/index.tsx +3 -0
- package/src/screens/PearlChaptersScreen.tsx +328 -0
- package/src/screens/PearlReaderScreen.tsx +304 -0
- package/src/screens/PearlsScreen.tsx +145 -0
- package/src/screens/QbankScreen.tsx +14 -2
- package/src/screens/QuestionScreen.tsx +28 -14
- package/src/screens/QuizStartScreen.tsx +45 -10
- package/src/screens/ResultScreen.tsx +13 -8
- package/src/screens/TopicListScreen.tsx +40 -7
- package/src/screens/index.ts +3 -0
- package/src/screens/pearlsMockData.ts +70 -0
- package/src/types/index.ts +17 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
ScrollView,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
SafeAreaView,
|
|
8
|
+
StatusBar,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import { useElearnConfig } from '../context/ElearnConfigContext';
|
|
11
|
+
import { useTheme } from '../context/ThemeContext';
|
|
12
|
+
import { Typography, Card, Icon } from '../components';
|
|
13
|
+
import { MOCK_PEARLS_TOPICS } from './pearlsMockData';
|
|
14
|
+
import type { PearlTopic } from '../types';
|
|
15
|
+
|
|
16
|
+
interface PearlsScreenProps {
|
|
17
|
+
onBack?: () => void;
|
|
18
|
+
hideHeader?: boolean;
|
|
19
|
+
onSelectTopic?: (topic: PearlTopic) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const PearlsScreen: React.FC<PearlsScreenProps> = ({
|
|
23
|
+
onBack,
|
|
24
|
+
hideHeader = false,
|
|
25
|
+
onSelectTopic,
|
|
26
|
+
}) => {
|
|
27
|
+
const { callbacks } = useElearnConfig();
|
|
28
|
+
const theme = useTheme();
|
|
29
|
+
|
|
30
|
+
const handleBackPress = () => {
|
|
31
|
+
if (onBack) {
|
|
32
|
+
onBack();
|
|
33
|
+
} else {
|
|
34
|
+
callbacks.onBackToDashboard?.();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<SafeAreaView style={[styles.safeArea, { backgroundColor: theme.background }]}>
|
|
40
|
+
<StatusBar barStyle="dark-content" />
|
|
41
|
+
|
|
42
|
+
{/* Header */}
|
|
43
|
+
{!hideHeader && (
|
|
44
|
+
<View style={styles.header}>
|
|
45
|
+
<TouchableOpacity onPress={handleBackPress} style={styles.backButton}>
|
|
46
|
+
<Icon name="arrow-left" size={20} color={theme.textPrimary} />
|
|
47
|
+
</TouchableOpacity>
|
|
48
|
+
|
|
49
|
+
<Typography variant="h2" bold style={styles.headerTitle}>
|
|
50
|
+
Pearls
|
|
51
|
+
</Typography>
|
|
52
|
+
|
|
53
|
+
<TouchableOpacity style={styles.headerRightBtn}>
|
|
54
|
+
<Icon name="search" size={20} color={theme.textPrimary} />
|
|
55
|
+
</TouchableOpacity>
|
|
56
|
+
</View>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{/* Topics list view */}
|
|
60
|
+
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
|
61
|
+
<Typography variant="h2" bold color={theme.textPrimary} style={styles.sectionHeaderTitle}>
|
|
62
|
+
All Topics
|
|
63
|
+
</Typography>
|
|
64
|
+
|
|
65
|
+
<View style={styles.listContainer}>
|
|
66
|
+
{MOCK_PEARLS_TOPICS.map((topic) => (
|
|
67
|
+
<TouchableOpacity
|
|
68
|
+
key={topic.id}
|
|
69
|
+
activeOpacity={0.8}
|
|
70
|
+
onPress={() => onSelectTopic?.(topic)}
|
|
71
|
+
>
|
|
72
|
+
<Card bordered style={styles.topicCard}>
|
|
73
|
+
<View style={styles.topicRow}>
|
|
74
|
+
{/* Left Circle Icon */}
|
|
75
|
+
<View style={[styles.iconCircle, { backgroundColor: 'rgba(59, 130, 246, 0.08)' }]}>
|
|
76
|
+
<Icon name="file-text" size={16} color="#3B82F6" />
|
|
77
|
+
</View>
|
|
78
|
+
{/* Right label */}
|
|
79
|
+
<Typography variant="body" bold color={theme.textPrimary} style={styles.topicLabelText}>
|
|
80
|
+
{topic.title}
|
|
81
|
+
</Typography>
|
|
82
|
+
</View>
|
|
83
|
+
</Card>
|
|
84
|
+
</TouchableOpacity>
|
|
85
|
+
))}
|
|
86
|
+
</View>
|
|
87
|
+
</ScrollView>
|
|
88
|
+
</SafeAreaView>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const styles = StyleSheet.create({
|
|
93
|
+
safeArea: {
|
|
94
|
+
flex: 1,
|
|
95
|
+
},
|
|
96
|
+
header: {
|
|
97
|
+
flexDirection: 'row',
|
|
98
|
+
justifyContent: 'space-between',
|
|
99
|
+
alignItems: 'center',
|
|
100
|
+
paddingHorizontal: 16,
|
|
101
|
+
paddingVertical: 12,
|
|
102
|
+
},
|
|
103
|
+
backButton: {
|
|
104
|
+
padding: 6,
|
|
105
|
+
},
|
|
106
|
+
headerTitle: {
|
|
107
|
+
fontSize: 18,
|
|
108
|
+
fontWeight: '700',
|
|
109
|
+
},
|
|
110
|
+
headerRightBtn: {
|
|
111
|
+
padding: 6,
|
|
112
|
+
},
|
|
113
|
+
scrollContent: {
|
|
114
|
+
paddingHorizontal: 16,
|
|
115
|
+
paddingTop: 8,
|
|
116
|
+
paddingBottom: 24,
|
|
117
|
+
},
|
|
118
|
+
sectionHeaderTitle: {
|
|
119
|
+
fontSize: 16,
|
|
120
|
+
marginBottom: 16,
|
|
121
|
+
},
|
|
122
|
+
listContainer: {
|
|
123
|
+
marginTop: 4,
|
|
124
|
+
},
|
|
125
|
+
topicCard: {
|
|
126
|
+
padding: 14,
|
|
127
|
+
marginBottom: 10,
|
|
128
|
+
borderRadius: 12,
|
|
129
|
+
},
|
|
130
|
+
topicRow: {
|
|
131
|
+
flexDirection: 'row',
|
|
132
|
+
alignItems: 'center',
|
|
133
|
+
},
|
|
134
|
+
iconCircle: {
|
|
135
|
+
width: 36,
|
|
136
|
+
height: 36,
|
|
137
|
+
borderRadius: 18,
|
|
138
|
+
justifyContent: 'center',
|
|
139
|
+
alignItems: 'center',
|
|
140
|
+
marginRight: 14,
|
|
141
|
+
},
|
|
142
|
+
topicLabelText: {
|
|
143
|
+
fontSize: 14,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
@@ -12,13 +12,19 @@ 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;
|
|
18
19
|
onBack?: () => void;
|
|
20
|
+
onSelectCourse?: (course: Course) => void;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
export const QbankScreen: React.FC<QbankScreenProps> = ({
|
|
23
|
+
export const QbankScreen: React.FC<QbankScreenProps> = ({
|
|
24
|
+
hideHeader = false,
|
|
25
|
+
onBack,
|
|
26
|
+
onSelectCourse,
|
|
27
|
+
}) => {
|
|
22
28
|
const { config, callbacks } = useElearnConfig();
|
|
23
29
|
const theme = useTheme();
|
|
24
30
|
|
|
@@ -154,7 +160,13 @@ export const QbankScreen: React.FC<QbankScreenProps> = ({ hideHeader = false, on
|
|
|
154
160
|
<Card
|
|
155
161
|
key={course.id}
|
|
156
162
|
bordered
|
|
157
|
-
onPress={() =>
|
|
163
|
+
onPress={() => {
|
|
164
|
+
if (onSelectCourse) {
|
|
165
|
+
onSelectCourse(course);
|
|
166
|
+
} else {
|
|
167
|
+
callbacks.onSelectCourse?.(course);
|
|
168
|
+
}
|
|
169
|
+
}}
|
|
158
170
|
style={styles.subjectCard}
|
|
159
171
|
>
|
|
160
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
|
|
18
|
-
questions
|
|
19
|
-
title
|
|
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 (!
|
|
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 =
|
|
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 <
|
|
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 =
|
|
79
|
-
const accuracy = Math.round((correctCount /
|
|
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:
|
|
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
|
-
|
|
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}/{
|
|
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
|
|
16
|
-
|
|
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 (
|
|
31
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
118
|
+
{activeTopic.title}
|
|
84
119
|
</Typography>
|
|
85
120
|
<Typography variant="caption" color={theme.textSecondary}>
|
|
86
|
-
{
|
|
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={!
|
|
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
|
|
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 <
|
|
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 =
|
|
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}/{
|
|
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 ===
|
|
219
|
+
disabled={selectedReviewIndex === activeQuestions.length - 1}
|
|
215
220
|
style={[
|
|
216
221
|
styles.nextActionBtn,
|
|
217
222
|
{ backgroundColor: theme.primary },
|
|
218
|
-
selectedReviewIndex ===
|
|
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
|
-
{
|
|
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
|
|
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
|
-
|
|
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
|
-
{
|
|
99
|
+
{activeCourse.title}
|
|
73
100
|
</Typography>
|
|
74
101
|
<Typography variant="body" color={theme.textSecondary}>
|
|
75
|
-
{
|
|
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 ===
|
|
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={() =>
|
|
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}>
|
package/src/screens/index.ts
CHANGED
|
@@ -4,3 +4,6 @@ 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';
|
|
8
|
+
export { PearlChaptersScreen } from './PearlChaptersScreen';
|
|
9
|
+
export { PearlReaderScreen } from './PearlReaderScreen';
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { PearlTopic } from '../types';
|
|
2
|
+
|
|
3
|
+
export const MOCK_PEARLS_TOPICS: PearlTopic[] = [
|
|
4
|
+
{
|
|
5
|
+
id: 'topic_head_neck',
|
|
6
|
+
title: 'Head and neck',
|
|
7
|
+
sections: {
|
|
8
|
+
'Head and Neck': [
|
|
9
|
+
{
|
|
10
|
+
id: 'hn_c1',
|
|
11
|
+
number: 1,
|
|
12
|
+
title: 'Classify sino-nasal neoplasam. Discuss the newer entities',
|
|
13
|
+
isFree: true,
|
|
14
|
+
bullets: [
|
|
15
|
+
'Benign Neoplasms: Benign sino-nasal tumors are generally slow-growing and less likely to invade surrounding structures. Common benign types include inverted papilloma, oncocytic Schneiderian papilloma, and fungiform papilloma, all of which originate from the Schneiderian mucosa. Inverted papilloma is notable for its local aggressiveness and high recurrence rate, sometimes exhibiting malignant transformation into squamous cell carcinoma.',
|
|
16
|
+
'Malignant Neoplasms: Malignant tumors of the sino-nasal tract are rare but clinically significant due to their aggressive behavior, late presentation, and complex anatomical constraints that limit surgical access. The most common malignant tumor is squamous cell carcinoma, often arising from dysplastic or papillomatous lesions.',
|
|
17
|
+
'Esthesioneuroblastoma: A notable entity originating from the olfactory epithelium and displaying variable degrees of malignancy. Additionally, melanoma, lymphoma, and undifferentiated carcinoma can also occur in the sino-nasal region, often presenting with nasal obstruction, epistaxis, and facial pain.'
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'hn_c2',
|
|
22
|
+
number: 2,
|
|
23
|
+
title: 'Histologic variants of ameloblastoma',
|
|
24
|
+
isLocked: true,
|
|
25
|
+
bullets: [
|
|
26
|
+
'Ameloblastoma is a benign but locally aggressive odontogenic neoplasm.',
|
|
27
|
+
'Follicular and plexiform are the most common histologic patterns.',
|
|
28
|
+
'Unicystic variants have a better prognosis and lower recurrence rates.'
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'hn_c3',
|
|
33
|
+
number: 3,
|
|
34
|
+
title: 'Role of brush ctology in diagnosis of oral cancerous and precancerous lessions',
|
|
35
|
+
isLocked: true,
|
|
36
|
+
bullets: [
|
|
37
|
+
'Oral brush cytology is a non-invasive screening aid.',
|
|
38
|
+
'High sensitivity for identifying cellular dysplasias.',
|
|
39
|
+
'Must be followed by scalpel biopsy for definitive diagnosis.'
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
'Eye': [
|
|
44
|
+
{
|
|
45
|
+
id: 'eye_c1',
|
|
46
|
+
number: 1,
|
|
47
|
+
title: 'Classify sino-nasal neoplasam. Discuss the newer entities',
|
|
48
|
+
isLocked: true,
|
|
49
|
+
bullets: []
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'eye_c2',
|
|
53
|
+
number: 2,
|
|
54
|
+
title: 'Histologic variants of ameloblastoma',
|
|
55
|
+
isLocked: true,
|
|
56
|
+
bullets: []
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
{ id: 'topic_brain_spine', title: 'Brain, Spine and Meninges', sections: {} },
|
|
62
|
+
{ id: 'topic_breast', title: 'Breast Pathology', sections: {} },
|
|
63
|
+
{ id: 'topic_skin', title: 'Skin and Connective Tissue', sections: {} },
|
|
64
|
+
{ id: 'topic_kidney', title: 'Kidney & Urinary Tract', sections: {} },
|
|
65
|
+
{ id: 'topic_gastro', title: 'Gastro-Intestinal System', sections: {} },
|
|
66
|
+
{ id: 'topic_endocrine', title: 'Endocrine Pathology', sections: {} },
|
|
67
|
+
{ id: 'topic_general', title: 'General Pathology', sections: {} },
|
|
68
|
+
{ id: 'topic_male_genital', title: 'Male Genital System', sections: {} },
|
|
69
|
+
{ id: 'topic_thoracic', title: 'Thoracic Pathology', sections: {} }
|
|
70
|
+
];
|
package/src/types/index.ts
CHANGED
|
@@ -189,3 +189,20 @@ export interface HomeScreenParams {
|
|
|
189
189
|
continueLearning?: { title?: string; show?: boolean };
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
|
+
|
|
193
|
+
export interface PearlChapter {
|
|
194
|
+
id: string;
|
|
195
|
+
number: number;
|
|
196
|
+
title: string;
|
|
197
|
+
isFree?: boolean;
|
|
198
|
+
isLocked?: boolean;
|
|
199
|
+
bullets: string[];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface PearlTopic {
|
|
203
|
+
id: string;
|
|
204
|
+
title: string;
|
|
205
|
+
sections?: {
|
|
206
|
+
[sectionName: string]: PearlChapter[];
|
|
207
|
+
};
|
|
208
|
+
}
|