Lessons
Просмотр уроков с видео, текстом и ресурсами.
Структура
src/features/lessons/
├── api/
│ └── queries.ts # fetchLesson()
├── hooks/
│ └── use-lesson.ts # Загрузить урок
├── components/
│ ├── lesson-header.tsx # Заголовок урока
│ ├── lesson-tabs.tsx # Вкладки (Content, Resources, Quiz)
│ ├── lesson-content.tsx # Основной контент
│ ├── lesson-videos.tsx # Список видео
│ ├── lesson-resources.tsx # Ссылки на ресурсы
│ ├── video-player.tsx # YouTube плеер
│ ├── video-card.tsx # Карточка видео
│ └── resource-link.tsx # Ссылка на ресурс
├── utils/
│ └── format-duration.ts # Форматирование времени
├── types.ts
└── index.ts
Доменная модель
Lesson
├── id: string
├── title: string
├── content: string (markdown)
├── duration: number (секунды)
├── moduleId: string
├── order: number
├── videos: Video[]
│ ├── id: string
│ ├── title: string
│ ├── youtubeId: string
│ └── duration: number
└── resources: Resource[]
├── id: string
├── title: string
├── url: string
└── type: 'article' | 'video' | 'tool'
useLesson Hook
import { useLesson } from '@/features/lessons';
function LessonScreen() {
const { id } = useLocalSearchParams();
const { data: lesson, isLoading } = useLesson(id);
if (isLoading) return <LoadingSpinner />;
return (
<View>
<LessonHeader lesson={lesson} />
<LessonTabs lesson={lesson} />
</View>
);
}
Компоненты
LessonTabs
Вкладки для разных типов контента:
<LessonTabs lesson={lesson}>
<LessonTabs.Tab name="content" label="Содержание">
<LessonContent content={lesson.content} />
</LessonTabs.Tab>
<LessonTabs.Tab name="videos" label="Видео">
<LessonVideos videos={lesson.videos} />
</LessonTabs.Tab>
<LessonTabs.Tab name="resources" label="Ресурсы">
<LessonResources resources={lesson.resources} />
</LessonTabs.Tab>
</LessonTabs>
VideoPlayer
YouTube embed через react-native-youtube-iframe:
import YoutubePlayer from 'react-native-youtube-iframe';
function VideoPlayer({ youtubeId, onProgress }) {
return (
<YoutubePlayer
height={220}
videoId={youtubeId}
onChangeState={(state) => {
if (state === 'ended') {
onProgress(100);
}
}}
/>
);
}
LessonContent
Markdown рендеринг с EduAI footer:
import Markdown from 'react-native-markdown-display';
function LessonContent({ content }) {
const { t } = useTranslation();
return (
<ScrollView className="flex-1 p-4">
<Markdown style={markdownStyles}>
{content}
</Markdown>
{/* Completion section */}
{showCompletion && (
<View className="mt-8 pt-6 border-t border-border">
<CompletionButton lessonId={lessonId} />
<RatingInput lessonId={lessonId} />
</View>
)}
{/* EduAI Footer - branding at bottom of lesson */}
<View className="mt-12 pt-8 border-t border-border">
<View className="flex-row items-center gap-4">
<View className="h-12 w-12 items-center justify-center rounded-xl bg-muted">
<Text className="text-lg font-bold">
<Text className="text-foreground">E</Text>
<Text className="text-primary">AI</Text>
</Text>
</View>
<View>
<Text className="text-lg font-semibold">
<Text className="text-foreground">Edu</Text>
<Text className="text-primary">AI</Text>
</Text>
<Text className="text-sm text-muted-foreground">
{t('course.aiFooter.createdWithAI')}
</Text>
</View>
</View>
</View>
</ScrollView>
);
}
Zustand Store
lesson-viewer-store.ts хранит состояние просмотра:
interface LessonViewerState {
activeTab: 'content' | 'videos' | 'resources';
scrollPosition: number;
setActiveTab: (tab: string) => void;
setScrollPosition: (position: number) => void;
}
Интеграция с Progress
См. Progress для отслеживания прогресса урока.
function LessonScreen() {
const { id } = useLocalSearchParams();
const { data: lesson } = useLesson(id);
const { trackScroll, trackTime, trackVideo } = useProgressTracking(id);
return (
<ScrollView onScroll={trackScroll}>
<LessonContent content={lesson.content} />
<VideoPlayer onProgress={trackVideo} />
</ScrollView>
);
}
Экраны
| Экран | Путь | Описание |
|---|---|---|
| Lesson Viewer | (app)/lesson/[id] | Полный экран урока |
Query Keys
['lesson', lessonId] // Один урок с видео и ресурсами
['lessons', moduleId] // Уроки модуля (для навигации)