learning 1. Juli 2024

Learning Progress Tracker

React Hook für Lernfortschritt-Tracking mit localStorage Persistenz und Statistiken.

reacthookstypescriptlocalStorage

useProgress Hook

Custom React Hook der den Lernfortschritt trackt, in localStorage persistiert, und Statistiken berechnet.

import { useState, useEffect, useCallback } from 'react';

interface ProgressEntry {
  moduleId: string;
  completedLessons: string[];
  totalLessons: number;
  lastAccessed: string;
  timeSpentMinutes: number;
}

interface ProgressStats {
  totalModules: number;
  completedModules: number;
  overallPercent: number;
  totalTimeHours: number;
  streak: number;
}

const STORAGE_KEY = 'learning_progress';

export function useProgress() {
  const [progress, setProgress] = useState<Record<string, ProgressEntry>>({});

  useEffect(() => {
    const stored = localStorage.getItem(STORAGE_KEY);
    if (stored) {
      setProgress(JSON.parse(stored));
    }
  }, []);

  const save = useCallback((data: Record<string, ProgressEntry>) => {
    localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
    setProgress(data);
  }, []);

  const completeLesson = useCallback((moduleId: string, lessonId: string, totalLessons: number) => {
    setProgress((prev) => {
      const entry = prev[moduleId] || {
        moduleId,
        completedLessons: [],
        totalLessons,
        lastAccessed: new Date().toISOString(),
        timeSpentMinutes: 0,
      };

      if (entry.completedLessons.includes(lessonId)) return prev;

      const updated = {
        ...prev,
        [moduleId]: {
          ...entry,
          completedLessons: [...entry.completedLessons, lessonId],
          lastAccessed: new Date().toISOString(),
        },
      };

      localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
      return updated;
    });
  }, []);

  const addTime = useCallback((moduleId: string, minutes: number) => {
    setProgress((prev) => {
      const entry = prev[moduleId];
      if (!entry) return prev;

      const updated = {
        ...prev,
        [moduleId]: { ...entry, timeSpentMinutes: entry.timeSpentMinutes + minutes },
      };

      localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
      return updated;
    });
  }, []);

  const getStats = useCallback((): ProgressStats => {
    const entries = Object.values(progress);
    const completedModules = entries.filter(
      (e) => e.completedLessons.length >= e.totalLessons
    ).length;

    const totalLessons = entries.reduce((sum, e) => sum + e.totalLessons, 0);
    const completedLessons = entries.reduce((sum, e) => sum + e.completedLessons.length, 0);

    return {
      totalModules: entries.length,
      completedModules,
      overallPercent: totalLessons > 0 ? Math.round((completedLessons / totalLessons) * 100) : 0,
      totalTimeHours: Math.round(entries.reduce((sum, e) => sum + e.timeSpentMinutes, 0) / 60 * 10) / 10,
      streak: calculateStreak(entries),
    };
  }, [progress]);

  return { progress, completeLesson, addTime, getStats, reset: () => save({}) };
}

function calculateStreak(entries: ProgressEntry[]): number {
  const dates = entries
    .map((e) => e.lastAccessed.split('T')[0])
    .sort()
    .reverse();

  const unique = [...new Set(dates)];
  let streak = 0;
  const today = new Date();

  for (let i = 0; i < unique.length; i++) {
    const expected = new Date(today);
    expected.setDate(expected.getDate() - i);
    const expectedStr = expected.toISOString().split('T')[0];

    if (unique[i] === expectedStr) {
      streak++;
    } else {
      break;
    }
  }

  return streak;
}