feat: improve learning and platform operations
This commit is contained in:
@@ -19,7 +19,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { Course, Comment, Lesson, Attachment } from '../types';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { getComments, getCompletedLessonIds, saveComment, saveLessonCompletion, incrementViews } from '../services/db';
|
||||
import { getComments, getLessonProgress, saveComment, saveLessonCompletion, saveLessonProgress, incrementViews } from '../services/db';
|
||||
import { LoginModal } from './LoginModal';
|
||||
import { MaterialCard } from './MaterialCard';
|
||||
|
||||
@@ -34,21 +34,30 @@ export const CoursePlayerModal: React.FC<CoursePlayerModalProps> = ({ course, on
|
||||
const [newComment, setNewComment] = useState('');
|
||||
const [currentLesson, setCurrentLesson] = useState<Lesson | null>(null);
|
||||
const [completedLessonIds, setCompletedLessonIds] = useState<string[]>([]);
|
||||
const [watchedSecondsByLesson, setWatchedSecondsByLesson] = useState<Record<string, number>>({});
|
||||
const [showLoginPrompt, setShowLoginPrompt] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'playlist' | 'materials' | 'discussion'>('playlist');
|
||||
const [copiedLink, setCopiedLink] = useState(false);
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const lastProgressSave = useRef<Record<string, number>>({});
|
||||
|
||||
// Load course data and this learner's saved state from PostgreSQL.
|
||||
useEffect(() => {
|
||||
if (course) {
|
||||
incrementViews(course.id);
|
||||
setCompletedLessonIds([]);
|
||||
setWatchedSecondsByLesson({});
|
||||
lastProgressSave.current = {};
|
||||
if (user) {
|
||||
getComments(course.id).then(setComments).catch(() => setComments([]));
|
||||
getCompletedLessonIds(course.id).then((completed) => {
|
||||
getLessonProgress(course.id).then((progress) => {
|
||||
const completed = progress.filter((item) => item.completedAt).map((item) => item.lessonId);
|
||||
setCompletedLessonIds(completed);
|
||||
setWatchedSecondsByLesson(Object.fromEntries(progress.map((item) => [item.lessonId, item.watchedSeconds])));
|
||||
lastProgressSave.current = Object.fromEntries(progress.map((item) => [item.lessonId, item.watchedSeconds]));
|
||||
const nextIncomplete = course.lessons.find((lesson) => !completed.includes(lesson.id));
|
||||
if (nextIncomplete) setCurrentLesson(nextIncomplete);
|
||||
}).catch(() => setCompletedLessonIds([]));
|
||||
} else {
|
||||
setComments([]);
|
||||
@@ -78,17 +87,21 @@ export const CoursePlayerModal: React.FC<CoursePlayerModalProps> = ({ course, on
|
||||
updateCompletedLessons(updated);
|
||||
saveLessonCompletion(lessonId, false).catch(() => updateCompletedLessons(completedLessonIds));
|
||||
} else {
|
||||
const updated = [...completedLessonIds, lessonId];
|
||||
updateCompletedLessons(updated);
|
||||
saveLessonCompletion(lessonId, true).catch(() => updateCompletedLessons(completedLessonIds));
|
||||
markLessonCompleted(lessonId);
|
||||
}
|
||||
};
|
||||
|
||||
const markLessonCompleted = (lessonId: string, watchedSeconds?: number) => {
|
||||
if (!user || completedLessonIds.includes(lessonId)) return;
|
||||
const previous = completedLessonIds;
|
||||
updateCompletedLessons([...previous, lessonId]);
|
||||
saveLessonCompletion(lessonId, true, watchedSeconds).catch(() => updateCompletedLessons(previous));
|
||||
};
|
||||
|
||||
// Video finished automatically -> mark lesson as completed
|
||||
const handleVideoEnded = () => {
|
||||
if (currentLesson && !completedLessonIds.includes(currentLesson.id)) {
|
||||
const updated = [...completedLessonIds, currentLesson.id];
|
||||
updateCompletedLessons(updated);
|
||||
markLessonCompleted(currentLesson.id, Math.round(videoRef.current?.duration || 0));
|
||||
}
|
||||
|
||||
// Auto-advance to next lesson if available
|
||||
@@ -110,16 +123,27 @@ export const CoursePlayerModal: React.FC<CoursePlayerModalProps> = ({ course, on
|
||||
if (videoRef.current) {
|
||||
const { currentTime, duration } = videoRef.current;
|
||||
if (duration > 0) {
|
||||
if (user && currentLesson && currentTime - (lastProgressSave.current[currentLesson.id] || 0) >= 30) {
|
||||
const watchedSeconds = Math.floor(currentTime);
|
||||
lastProgressSave.current[currentLesson.id] = watchedSeconds;
|
||||
setWatchedSecondsByLesson((current) => ({ ...current, [currentLesson.id]: watchedSeconds }));
|
||||
saveLessonProgress(currentLesson.id, watchedSeconds).catch(() => undefined);
|
||||
}
|
||||
const percent = Math.round((currentTime / duration) * 100);
|
||||
// Automatically mark as complete when reaching 95%+
|
||||
if (percent >= 95 && currentLesson && !completedLessonIds.includes(currentLesson.id)) {
|
||||
const updated = [...completedLessonIds, currentLesson.id];
|
||||
updateCompletedLessons(updated);
|
||||
markLessonCompleted(currentLesson.id, Math.round(currentTime));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const restoreWatchPosition = () => {
|
||||
if (!currentLesson || !videoRef.current) return;
|
||||
const watchedSeconds = watchedSecondsByLesson[currentLesson.id] || 0;
|
||||
if (watchedSeconds > 0 && watchedSeconds < videoRef.current.duration - 5) videoRef.current.currentTime = watchedSeconds;
|
||||
};
|
||||
|
||||
const handleAddComment = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newComment.trim() || !course) return;
|
||||
@@ -224,6 +248,7 @@ export const CoursePlayerModal: React.FC<CoursePlayerModalProps> = ({ course, on
|
||||
autoPlay
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
onEnded={handleVideoEnded}
|
||||
onLoadedMetadata={restoreWatchPosition}
|
||||
>
|
||||
Seu navegador não suporta a tag de vídeo.
|
||||
</video>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { User as UserIcon, LogOut, Search, Folder } from 'lucide-react';
|
||||
import { User as UserIcon, LogOut, Search, Folder, BookOpenCheck } from 'lucide-react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { LoginModal } from './LoginModal';
|
||||
import { SearchModal } from './SearchModal';
|
||||
@@ -100,6 +100,7 @@ export const Navbar: React.FC<NavbarProps> = ({ onPlay }) => {
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center gap-3">
|
||||
{user.role === 'student' && <button onClick={() => navigate('/meu-aprendizado')} className={`hidden sm:inline-flex items-center gap-2 text-sm font-semibold px-4 py-2.5 rounded-[980px] transition-colors ${location.pathname === '/meu-aprendizado' ? 'bg-orange-500/20 text-orange-400 border border-orange-500/30' : 'text-gray-300 hover:text-white bg-white/5 hover:bg-white/10'}`}><BookOpenCheck className="w-4 h-4" /> Meu aprendizado</button>}
|
||||
{user.role === 'superadmin' ? (
|
||||
<button
|
||||
onClick={() => navigate('/admin')}
|
||||
@@ -150,4 +151,3 @@ export const Navbar: React.FC<NavbarProps> = ({ onPlay }) => {
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user