Перейти к основному содержимому

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>;
}

Стратегия синхронизации

  1. Обновления накапливаются в store
  2. Debounce 5 секунд перед отправкой
  3. При уходе с экрана — немедленная синхронизация
  4. При потере сети — сохранение локально, повтор позже
// 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] // Прогресс всех уроков курса