Free
Open source • Copy & paste

Lingo Learner

Gamified lessons, streak headers, and a skill-tree layout for daily practice.

3 screens
Expo Router + StyleSheet + Reanimated
Motion-ready
streaksskill treelessons
Browse more kits

Source code

Explore the component structure. Copy directly into any Expo app.

import { Stack, useRouter } from 'expo-router';
import React from 'react';
import { Dimensions, ScrollView, View } from 'react-native';
import Animated, { FadeIn } from 'react-native-reanimated';
import Svg, { Path } from 'react-native-svg';

import { SafeAreaView, Text } from '@/components/ui';

import { MOCK_UNITS } from '../constants';
import SkillButton from '../components/skill-button';
import XPHeader from '../components/xp-header';

const { width } = Dimensions.get('window');

import { useThemeConfig } from '@/lib/use-theme-config';

// Constants for positioning
const NODE_SIZE = 72;
const NODE_SPACING = 20;
const X_AMPLITUDE = 60; // Max horizontal offset

export default function DuolingoHomeScreen() {
  const router = useRouter();
  const theme = useThemeConfig();

  return (
    <View
      className="flex-1"
      style={{ backgroundColor: theme.colors.background }}
    >
      <Stack.Screen options={{ headerShown: false }} />
      <XPHeader />

      <ScrollView
        showsVerticalScrollIndicator={false}
        contentContainerStyle={{ paddingBottom: 100, paddingTop: 100 }}
      >
        {MOCK_UNITS.map((unit, unitIndex) => {
          // Generate path for this unit
          let pathD = '';
          // We need to know previous point to curve from
          // For simplicity, we just draw straight lines or simple bezier curve between centers

          // To do this properly in one SVG:
          // Calculate all center points first
          const points = unit.lessons.map((_, i) => {
            const offset = Math.sin((i * Math.PI) / 2) * X_AMPLITUDE;
            // Vertical position relative to unit container top
            // But since we map simple views, capturing exact Y is hard without onLayout.
            // We'll trust rigid spacing: (NODE_SIZE + NODE_SPACING) per item.
            const y = i * (NODE_SIZE + NODE_SPACING) + NODE_SIZE / 2;
            const x = width / 2 + offset;
            return { x, y };
          });

          // Construct path
          points.forEach((p, i) => {
            if (i === 0) {
              pathD += `M ${p.x} ${p.y}`;
            } else {
              const prev = points[i - 1];
              const c1y = prev.y + 40;
              const c2y = p.y - 40;
              pathD += ` C ${prev.x} ${c1y} ${p.x} ${c2y} ${p.x} ${p.y}`;
            }
          });

          return (
            <View key={unit.id} className="mb-0">
              {/* Unit Header */}
              <View
                className="px-4 py-6 mb-8 mx-4 rounded-2xl shadow-sm overflow-hidden relative"
                style={{ backgroundColor: unit.color }}
              >
                <View className="flex-row justify-between items-start relative z-10">
                  <View className="flex-1 mr-4">
                    <Text className="text-white/90 font-bold text-lg uppercase mb-1">
                      {unit.title}
                    </Text>
                    <Text className="text-white font-black text-xl leading-tight">
                      {unit.description}
                    </Text>
                  </View>
                  <View className="bg-white/20 px-3 py-2 rounded-xl border border-white/10">
                    <Text className="text-white font-bold">📖 Guide</Text>
                  </View>
                </View>
                {/* Mascot Placeholder Bubble */}
                <View className="absolute -bottom-2 -right-2 opacity-20 transform rotate-12">
                  <Text style={{ fontSize: 80 }}>🦉</Text>
                </View>
              </View>

              {/* Path Container */}
              <View className="relative items-center">
                {/* The Connector Line */}
                <View
                  style={{
                    position: 'absolute',
                    top: 0,
                    left: 0,
                    right: 0,
                    height: points[points.length - 1].y,
                  }}
                >
                  <Svg width={width} height="100%">
                    <Path
                      d={pathD}
                      stroke={theme.colors.border}
                      strokeWidth="10"
                      strokeLinecap="round"
                      fill="none"
                    />
                  </Svg>
                </View>

                {/* The Nodes */}
                {unit.lessons.map((lesson, index) => {
                  const offset = Math.sin((index * Math.PI) / 2) * X_AMPLITUDE;

                  return (
                    <Animated.View
                      key={lesson.id}
                      entering={FadeIn.delay(index * 100).duration(500)}
                      style={{
                        transform: [{ translateX: offset }],
                        marginBottom: NODE_SPACING,
                        width: NODE_SIZE,
                        height: NODE_SIZE,
                      }}
                    >
                      <SkillButton
                        icon={lesson.icon}
                        status={lesson.status as any}
                        type={lesson.type as any}
                        stars={lesson.stars}
                        color={unit.color}
                        onPress={() => {
                          if (lesson.status !== 'locked') {
                            router.push({
                              pathname: '/screen-kits/duolingo/lesson',
                              params: { id: lesson.id },
                            });
                          }
                        }}
                      />
                    </Animated.View>
                  );
                })}
              </View>
            </View>
          );
        })}
      </ScrollView>
    </View>
  );
}

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