Screen kit gallery
Free
Open source • Copy & pasteLingo Learner
Gamified learning
Gamified lessons, streak headers, and a skill-tree layout for daily practice.
4 screens
Expo Router + StyleSheet + Reanimated + Lottie
Motion-ready
streaksskill treelessons
Source code
Explore the component structure. Copy directly into any Expo app.
import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
import LottieView from 'lottie-react-native';
import React, { useEffect, useRef } from 'react';
import { StyleSheet, TextInput, View, Dimensions } from 'react-native';
import Animated, {
FadeInDown,
FadeInUp,
SharedValue,
useAnimatedProps,
useDerivedValue,
useSharedValue,
withDelay,
withTiming,
ZoomIn,
} from 'react-native-reanimated';
import { Text } from '@/components/ui';
import { useThemeConfig } from '@/lib/use-theme-config';
import DuoButton from '../components/duo-button';
import { DUO_COLORS } from '../constants';
const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);
function ReText({ style, text }: { style?: any; text: SharedValue<string> }) {
const animatedProps = useAnimatedProps(() => {
return {
text: text.value,
} as any;
});
return (
<AnimatedTextInput
underlineColorAndroid="transparent"
editable={false}
defaultValue={text.value}
caretHidden
selectionColor="transparent"
contextMenuHidden
style={[styles.reText, style]}
animatedProps={animatedProps}
/>
);
}
function StatPillar({
label,
value,
color,
edgeColor,
icon,
delay = 0,
}: {
label: string;
value: string;
color: string;
edgeColor: string;
icon: any;
delay?: number;
}) {
const theme = useThemeConfig();
return (
<Animated.View
entering={FadeInDown.delay(delay).springify()}
style={styles.statPillarContainer}
>
<View
style={[
styles.statPillarBox,
{
backgroundColor: color,
borderBottomColor: edgeColor,
},
]}
>
<Ionicons name={icon} size={28} color="white" />
<Text style={styles.statPillarValue}>{value}</Text>
</View>
<Text
style={[
styles.statPillarLabel,
{ color: theme.colors.mutedForeground },
]}
>
{label}
</Text>
</Animated.View>
);
}
export default function LessonCompleteScreen() {
const router = useRouter();
const params = useLocalSearchParams();
const theme = useThemeConfig();
const confettiRef = useRef<LottieView>(null);
const xp = params.xp ? Number(params.xp) : 40;
const accuracyVal = params.accuracy ? Number(params.accuracy) : 100;
const baseXp = Math.floor(xp * 0.75);
const bonusXp = Math.ceil(xp * 0.25);
const totalXp = xp;
const focusMinutes = 5;
const accuracy = accuracyVal;
const rating = accuracy >= 90 ? 'Amazing' : accuracy >= 70 ? 'Good' : 'Okay';
const streakDays = 7;
const unitProgress = 6;
const unitTotal = 12;
const unitCompletion = Math.min(unitProgress / unitTotal, 1);
const showPerfectBadge = accuracy === 100;
const xpValue = useSharedValue(0);
useEffect(() => {
xpValue.value = withDelay(800, withTiming(totalXp, { duration: 1500 }));
setTimeout(() => {
confettiRef.current?.play();
}, 500);
}, [totalXp, xpValue]);
const xpText = useDerivedValue(() => {
return `${Math.round(xpValue.value)}`;
});
const backgroundGradient = theme.dark
? (['#0B0F14', '#0E1319', '#111214'] as const)
: (['#FFFDF6', '#FFFDF6', '#FFFFFF'] as const);
const headlineColor = theme.dark ? '#FFE7A3' : DUO_COLORS.goldDark;
return (
<View
style={[styles.container, { backgroundColor: theme.colors.background }]}
>
<Stack.Screen options={{ headerShown: false }} />
<LinearGradient
colors={backgroundGradient}
style={StyleSheet.absoluteFillObject}
/>
{/* Confetti Layer */}
<View pointerEvents="none" style={styles.confettiLayer}>
<LottieView
ref={confettiRef}
source={{
uri: 'https://assets9.lottiefiles.com/packages/lf20_u4yrau.json',
}}
autoPlay={false}
loop={false}
style={styles.lottie}
resizeMode="cover"
/>
</View>
<View style={styles.content}>
{/* Hero Section */}
<Animated.View
entering={ZoomIn.duration(600)}
style={styles.heroSection}
>
<View style={styles.trophyContainer}>
<View
style={[
styles.trophyGlow,
{ backgroundColor: DUO_COLORS.gold + '20' },
]}
>
<Ionicons name="trophy" size={100} color={DUO_COLORS.gold} />
</View>
</View>
<Text style={[styles.headline, { color: headlineColor }]}>
{showPerfectBadge ? 'Perfect!' : 'Well done!'}
</Text>
<Text
style={[
styles.subheadline,
{ color: theme.colors.mutedForeground },
]}
>
{showPerfectBadge
? "You didn't make a single mistake!"
: 'You are making great progress.'}
</Text>
</Animated.View>
{/* XP Counter Section */}
<Animated.View
entering={FadeInUp.delay(400).springify()}
style={styles.xpSection}
>
<View style={styles.xpRow}>
<ReText
text={xpText}
style={{
color: DUO_COLORS.gold,
fontSize: 72,
fontWeight: '900',
textAlign: 'center',
}}
/>
<Text style={styles.xpLabel}>XP</Text>
</View>
<Text
style={[
styles.xpBreakdown,
{ color: theme.colors.mutedForeground },
]}
>
Base {baseXp} + Bonus {bonusXp}
</Text>
</Animated.View>
{/* 3D Stats Pillars */}
<View style={styles.statsPillarsRow}>
<StatPillar
label="Focus"
value={`${focusMinutes}m`}
color={DUO_COLORS.blue}
edgeColor={DUO_COLORS.blueDark}
icon="timer"
delay={600}
/>
<StatPillar
label="Accuracy"
value={`${accuracy}%`}
color={DUO_COLORS.green}
edgeColor={DUO_COLORS.greenDark}
icon="checkmark-circle"
delay={700}
/>
<StatPillar
label="Rating"
value={rating}
color={DUO_COLORS.red}
edgeColor={DUO_COLORS.redDark}
icon="heart"
delay={800}
/>
</View>
{/* Progress Bar Section */}
<Animated.View
entering={FadeInUp.delay(900).springify()}
style={styles.progressSection}
>
<View
style={[
styles.progressCard,
{
backgroundColor: theme.dark
? 'rgba(255,255,255,0.05)'
: 'rgba(0,0,0,0.03)',
borderColor: theme.dark
? 'rgba(255,255,255,0.1)'
: 'rgba(0,0,0,0.05)',
},
]}
>
<View style={styles.progressHeader}>
<View style={styles.streakRow}>
<Ionicons name="flame" size={20} color={DUO_COLORS.red} />
<Text
style={[
styles.streakText,
{ color: theme.colors.foreground },
]}
>
{streakDays} DAY STREAK
</Text>
</View>
<Text
style={[
styles.progressCount,
{ color: theme.colors.mutedForeground },
]}
>
{unitProgress}/{unitTotal}
</Text>
</View>
<View
style={[
styles.progressTrack,
{
backgroundColor: theme.dark
? 'rgba(255,255,255,0.08)'
: 'rgba(0,0,0,0.08)',
},
]}
>
<View
style={[
styles.progressFill,
{
width: `${Math.round(unitCompletion * 100)}%`,
backgroundColor: DUO_COLORS.green,
},
]}
/>
</View>
</View>
</Animated.View>
</View>
{/* Footer */}
<Animated.View entering={FadeInUp.delay(1200)} style={styles.footer}>
<DuoButton
label="Continue"
onPress={() => router.dismissTo('/screen-kits/duolingo')}
size="lg"
variant="primary"
fullWidth
/>
</Animated.View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
confettiLayer: {
...StyleSheet.absoluteFillObject,
zIndex: 10,
elevation: 10,
},
lottie: {
flex: 1,
},
content: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 24,
paddingTop: 100,
},
heroSection: {
alignItems: 'center',
marginBottom: 20,
},
trophyContainer: {
alignItems: 'center',
justifyContent: 'center',
},
trophyGlow: {
width: 180,
height: 180,
borderWidth: 2,
borderColor: DUO_COLORS.gold + '40',
borderRadius: 90,
alignItems: 'center',
justifyContent: 'center',
},
headline: {
fontSize: 36,
fontWeight: '900',
textAlign: 'center',
marginTop: 12,
lineHeight: 48,
includeFontPadding: false,
},
subheadline: {
fontSize: 18,
fontWeight: '700',
textAlign: 'center',
marginTop: 8,
lineHeight: 26,
},
xpSection: {
alignItems: 'center',
marginBottom: 20,
},
xpRow: {
flexDirection: 'row',
alignItems: 'flex-end',
},
xpLabel: {
fontSize: 36,
fontWeight: '900',
marginBottom: 12,
marginLeft: 8,
color: DUO_COLORS.gold,
lineHeight: 48,
includeFontPadding: false,
},
xpBreakdown: {
fontWeight: '700',
textTransform: 'uppercase',
letterSpacing: 2,
fontSize: 12,
},
statsPillarsRow: {
flexDirection: 'row',
width: '100%',
justifyContent: 'space-between',
marginBottom: 40,
},
statPillarContainer: {
flex: 1,
alignItems: 'center',
paddingHorizontal: 4,
},
statPillarBox: {
width: '100%',
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
paddingTop: 16,
paddingBottom: 12,
borderBottomWidth: 6,
minHeight: 100,
},
statPillarValue: {
color: '#ffffff',
fontWeight: '900',
fontSize: 20,
marginTop: 4,
lineHeight: 28,
includeFontPadding: false,
},
statPillarLabel: {
fontWeight: '700',
textTransform: 'uppercase',
marginTop: 12,
textAlign: 'center',
fontSize: 10,
letterSpacing: 1.5,
},
progressSection: {
width: '100%',
},
progressCard: {
borderRadius: 24,
paddingHorizontal: 24,
paddingVertical: 20,
borderWidth: 2,
marginBottom: 24,
},
progressHeader: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 16,
},
streakRow: {
flexDirection: 'row',
alignItems: 'center',
},
streakText: {
fontWeight: '900',
marginLeft: 8,
fontSize: 18,
},
progressCount: {
fontWeight: '700',
},
progressTrack: {
height: 16,
borderRadius: 8,
overflow: 'hidden',
},
progressFill: {
height: '100%',
borderRadius: 8,
},
footer: {
paddingHorizontal: 20,
paddingBottom: 40,
paddingTop: 32,
borderTopWidth: 1,
borderTopColor: 'transparent',
},
reText: {
padding: 0,
textAlign: 'center',
textAlignVertical: 'center',
includeFontPadding: false,
fontVariant: ['tabular-nums'],
lineHeight: 90,
minHeight: 90,
},
});
Building a full app?
VibeFast Pro is a complete Expo + Next.js starter kit with authentication, payments (RevenueCat), AI features, backend (Convex/Supabase), and more. Ship your app in days, not months.