Progress
Отслеживание прогресса пользователя по урокам.
Структура
src/features/progress/
├── api/
│ ├── queries.ts # fetchLessonProgress()
│ └── mutations.ts # updateProgress()
├── hooks/
│ ├── use-lesson-progress.ts # Текущий прогресс
│ ├── use-progress-mutation.ts # Обновление прогресса
│ ├── use-scroll-tracking.ts # Трекинг скролла
│ ├── use-time-tracking.ts # Трекинг времени
│ ├── use-video-tracking.ts # Трекинг видео
│ └── use-progress-sync.ts # Синхронизация
├── components/
│ ├── completion-button.tsx # "Отметить завершённым"
│ └── rating-input.tsx # Оценка урока
├── types.ts
└── index.ts
Доменная модель
LessonProgress
├── id: string
├── lessonId: string
├── userId: string
├── completed: boolean
├── completedAt: Date | null
├── scrollProgress: number (0-100)
├── timeSpent: number (секунды)
├── videoProgress: number (0-100)
├── rating: number | null (1-5)
└── updatedAt: Date
Hooks
useLessonProgress
Получить текущий прогресс:
import { useLessonProgress } from '@/features/progress';
function LessonScreen({ lessonId }) {
const { data: progress } = useLessonProgress(lessonId);
return (
<View>
<ProgressBar value={progress?.scrollProgress || 0} />
<Text>Время: {formatDuration(progress?.timeSpent || 0)}</Text>
</View>
);
}
useProgressMutation
Обновить прогресс с optimistic update:
import { useProgressMutation } from '@/features/progress';
function LessonScreen({ lessonId }) {
const { mutate: updateProgress } = useProgressMutation();
const handleComplete = () => {
updateProgress({
lessonId,
completed: true,
completedAt: new Date(),
});
};
}
useScrollTracking
Автоматически отслеживает скролл:
import { useScrollTracking } from '@/features/progress';
function LessonContent({ lessonId, content }) {
const { onScroll, contentHeight, setContentHeight } = useScrollTracking(lessonId);
return (
<ScrollView
onScroll={onScroll}
onContentSizeChange={(w, h) => setContentHeight(h)}
scrollEventThrottle={16}
>
<Markdown>{content}</Markdown>
</ScrollView>
);
}
useTimeTracking
Отслеживает время на уроке:
import { useTimeTracking } from '@/features/progress';
function LessonScreen({ lessonId }) {
// Автоматически считает время пока экран активен
useTimeTracking(lessonId);
}
useVideoTracking
Отслеживает прогресс видео:
import { useVideoTracking } from '@/features/progress';
function VideoPlayer({ lessonId, youtubeId }) {
const { onProgress, onComplete } = useVideoTracking(lessonId);
return (
<YoutubePlayer
videoId={youtubeId}
onProgress={({ currentTime, duration }) => {
onProgress((currentTime / duration) * 100);
}}
onChangeState={(state) => {
if (state === 'ended') onComplete();
}}
/>
);
}
Zustand Store
lesson-progress-store.ts — временное хранение до синхронизации:
interface LessonProgressStore {
pendingUpdates: Map<string, ProgressUpdate>;
addUpdate: (lessonId: string, update: Partial<ProgressUpdate>) => void;
flushUpdates: () => Promise<void>;
}
Стратегия синхронизации
- Обновления накапливаются в store
- Debounce 5 секунд перед отправкой
- При уходе с экрана — немедленная синхронизация
- При потере сети — сохранение локально, повтор позже
// useProgressSync.ts
useEffect(() => {
const interval = setInterval(() => {
store.flushUpdates();
}, 5000);
return () => {
clearInterval(interval);
store.flushUpdates(); // Финальная синхронизация
};
}, []);
Компоненты
CompletionButton
<CompletionButton
completed={progress?.completed}
onComplete={handleComplete}
disabled={isPending}
/>
RatingInput
<RatingInput
value={progress?.rating}
onChange={(rating) => updateProgress({ lessonId, rating })}
/>
Query Keys
['progress', lessonId] // Прогресс одного урока
['progress', 'course', courseId] // Прогресс всех уроков курса