Free
Open source • Copy & paste

Lingo 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
Browse more kits

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.

Learn more