Achievement System Overview
Sunschool rewards learners with achievements (also called “trophies” or “badges”) for reaching milestones and demonstrating excellence. Achievements provide motivation and celebrate progress.
Types of Achievements
Achievements are awarded automatically when learners meet specific criteria.
Milestone Achievements
// From server/utils.ts:26-73
export function checkForAchievements ( lessonHistory : Lesson [], completedLesson ?: Lesson ) {
const achievements : {
type : string ;
payload : {
title : string ;
description : string ;
icon : string ;
};
}[] = [];
// First lesson completed
if ( lessonHistory . filter ( l => l . status === "DONE" ). length === 1 ) {
achievements . push ({
type: "FIRST_LESSON" ,
payload: {
title: "First Steps" ,
description: "Completed your very first lesson!" ,
icon: "award"
}
});
}
// 5 lessons completed
if ( lessonHistory . filter ( l => l . status === "DONE" ). length === 5 ) {
achievements . push ({
type: "FIVE_LESSONS" ,
payload: {
title: "Learning Explorer" ,
description: "Completed 5 lessons!" ,
icon: "book-open"
}
});
}
First Steps FIRST_LESSON Earned after completing your very first lesson. Celebrates the beginning of the learning journey.
Learning Explorer FIVE_LESSONS Awarded when a learner completes 5 total lessons. Shows commitment to learning.
Perfect Score! PERFECT_SCORE Given when a learner gets 100% on any quiz. Recognizes excellence and mastery.
Perfect Score Achievement
// From server/utils.ts:61-70
// Perfect score on a quiz
if ( completedLesson && completedLesson . score === 100 ) {
achievements . push ({
type: "PERFECT_SCORE" ,
payload: {
title: "Perfect Score!" ,
description: "Got all answers correct in a quiz!" ,
icon: "star"
}
});
}
Perfect Score achievements can be earned multiple times - once for each quiz where the learner answers every question correctly.
Achievement Database Schema
// From shared/schema.ts:178-195
export const achievements = pgTable ( "achievements" , {
id: uuid ( "id" ). defaultRandom (). primaryKey (),
learnerId: varchar ( "learner_id" ). notNull (). references (() => users . id , { onDelete: "cascade" }),
type: text ( "type" ). notNull (),
payload: json ( "payload" ). $type <{
title : string ;
description : string ;
icon : string ;
}>(),
awardedAt: timestamp ( "awarded_at" ). defaultNow (),
});
export const achievementsRelations = relations ( achievements , ({ one }) => ({
learner: one ( users , {
fields: [ achievements . learnerId ],
references: [ users . id ],
}),
}));
Achievement Fields
Field Type Description idUUID Unique identifier for each achievement learnerIdvarchar References the learner who earned it typetext Achievement type (e.g., “FIRST_LESSON”) payloadjson Contains title, description, and icon awardedAttimestamp When the achievement was earned
How to Earn Badges
Achievements are checked and awarded automatically after quiz completion:
// From server/routes.ts:1115-1124
// Check for achievements
const newAchievements = checkForAchievements ( lessonHistory , completedLesson );
// Award any new achievements
for ( const achievement of newAchievements ) {
await storage . createAchievement ({
learnerId: learnerId . toString (),
type: achievement . type ,
payload: achievement . payload
});
}
Earning Flow
Complete a Lesson
Learner reads through all sections of a lesson
Take the Quiz
Submit answers to all quiz questions
Server Checks Criteria
Backend runs checkForAchievements() function const newAchievements = checkForAchievements ( lessonHistory , completedLesson );
Achievements Awarded
Matching achievements are created in the database for ( const achievement of newAchievements ) {
await storage . createAchievement ({ ... });
}
Notification Shown
Learner sees achievement unlock animation
Achievement Notifications
When achievements are earned, learners see an exciting unlock animation.
Unlock Modal
// From quiz-page.tsx:228-238
{ quizScore ?. newAchievements && quizScore . newAchievements . length > 0 && (
< AchievementUnlock
achievements = {quizScore.newAchievements.map( a => ({
title: a . title || a . payload ?. title || 'Achievement Unlocked!' ,
description: a . description || a . payload ?. description ,
type: a . type ,
}))}
visible = { showAchievements }
onDismiss = {() => setShowAchievements ( false )}
/>
)}
Notification Timing
// From quiz-page.tsx:111-113
if ( data . newAchievements && data . newAchievements . length > 0 ) {
setTimeout (() => setShowAchievements ( true ), 1500 );
}
Achievement notifications appear 1.5 seconds after quiz results, giving learners time to see their score first.
Visual Presentation
// From quiz-page.tsx:399-411
{ quizScore . newAchievements && quizScore . newAchievements . length > 0 && (
< View style = {styles. achievementsContainer } >
< Text style = {styles. achievementsTitle } > Trophies Unlocked !</ Text >
{quizScore.newAchievements.map(( achievement , index) => (
<View key = { index } style = {styles. achievementItem } >
<View style = {styles. achievementIcon } >
<CheckCircle size = { 24 } color = "#FFD93D" />
</View>
<Text style = {styles. achievementText } > {achievement. title } </ Text >
</View>
))}
</ View >
)}
Viewing Trophy Collection
Learners can view all their earned achievements from the dashboard.
Recent Achievements Display
// From learner-dashboard.tsx:36-44
// Fetch achievements
const {
data : achievements ,
isLoading : isAchievementsLoading ,
error : achievementsError ,
} = useQuery ({
queryKey: [ '/api/achievements' ],
queryFn : () => apiRequest ( 'GET' , '/api/achievements' ). then ( res => res . data ),
});
Achievement Cards
// From learner-dashboard.tsx:161-188
< View style = {styles. sectionContainer } >
< View style = {styles. sectionHeader } >
< Text style = {styles. sectionTitle } > Recent Achievements </ Text >
< TouchableOpacity onPress = {() => navigation.navigate( 'ProgressPage' )}>
<Text style={styles.seeAllText}>See All</Text>
</TouchableOpacity>
</View>
{isAchievementsLoading ? (
<ActivityIndicator size= "large" color={colors.primary} />
) : recentAchievements . length > 0 ? (
< ScrollView horizontal showsHorizontalScrollIndicator = { false } >
{recentAchievements.map((achievement) => (
< AchievementBadge
key = {achievement. id }
achievement = { achievement }
style = {styles. achievementBadge }
/>
))}
</ ScrollView >
) : (
< View style = {styles. emptyState } >
< Award size = { 48 } color = {colors. primaryLight } />
< Text style = {styles. emptyStateText } > Complete lessons to earn achievements </ Text >
</ View >
)}
</ View >
Display Features
Dashboard View
Progress Page
Shows 3 most recent achievements
Horizontal scroll for easy browsing
“See All” link to full collection
Empty state with encouragement if none yet
Complete achievement history
Sorted by most recent first
Total count displayed
Organized by date earned
Achievement API Endpoints
Get Achievements
// From server/routes.ts:1224-1248
app . get ( "/api/achievements" , isAuthenticated , asyncHandler ( async ( req : AuthRequest , res ) => {
const user = req . user ;
if ( ! user ) {
return res . status ( 401 ). json ({ message: 'Not authenticated' });
}
// Get learner ID from query or use current user
const learnerId = req . query . learnerId ? Number ( req . query . learnerId ) : user . id ;
// If requesting another learner's achievements, verify authorization
if ( learnerId !== user . id ) {
// Check if user is authorized to view this learner's achievements
const learner = await storage . getUser ( learnerId );
if ( ! learner || ( learner . parentId !== user . id && user . role !== 'ADMIN' )) {
return res . status ( 403 ). json ({ message: 'Not authorized' });
}
}
const achievements = await storage . getAchievements ( learnerId );
res . json ( achievements );
}));
Authorization checks :
Learners can view their own achievements
Parents can view their children’s achievements
Admins can view all achievements
Parent View
Parents can see all achievements their children have earned:
// From server/routes.ts:1279-1317
const [ learner , profile , lessons , achievements ] = await Promise . all ([
storage . getUser ( learnerId ),
storage . getLearnerProfile ( learnerId ),
storage . getLessons ( learnerId ),
storage . getAchievements ( learnerId )
]);
// ... later in report generation
achiementsCount : achievements . length ,
Parents see:
Total achievement count in learner reports
Full achievement list with dates earned
Achievement types and descriptions
Progress over time
Next Steps