Free
Open source • Copy & pasteLingo Learner
Gamified lessons, streak headers, and a skill-tree layout for daily practice.
3 screens
Expo Router + StyleSheet + Reanimated
Motion-ready
streaksskill treelessons
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.