mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-06-06 18:31:00 +00:00
Implemented document progress tracking based on read time, got video progress tracking working with Youtube embeds
This commit is contained in:
parent
f7bbf93f95
commit
4585ed263c
@ -11,6 +11,7 @@ import { getTotalFromZaps } from "@/utils/lightning";
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import useWindowWidth from "@/hooks/useWindowWidth";
|
import useWindowWidth from "@/hooks/useWindowWidth";
|
||||||
import appConfig from "@/config/appConfig";
|
import appConfig from "@/config/appConfig";
|
||||||
|
import useTrackDocumentLesson from "@/hooks/tracking/useTrackDocumentLesson";
|
||||||
|
|
||||||
const MDDisplay = dynamic(
|
const MDDisplay = dynamic(
|
||||||
() => import("@uiw/react-markdown-preview"),
|
() => import("@uiw/react-markdown-preview"),
|
||||||
@ -22,10 +23,19 @@ const MDDisplay = dynamic(
|
|||||||
const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
|
const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
|
||||||
const [zapAmount, setZapAmount] = useState(0);
|
const [zapAmount, setZapAmount] = useState(0);
|
||||||
const [nAddress, setNAddress] = useState(null);
|
const [nAddress, setNAddress] = useState(null);
|
||||||
|
const [completed, setCompleted] = useState(false);
|
||||||
const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: lesson, type: "lesson" });
|
const { zaps, zapsLoading, zapsError } = useZapsQuery({ event: lesson, type: "lesson" });
|
||||||
const { returnImageProxy } = useImageProxy();
|
const { returnImageProxy } = useImageProxy();
|
||||||
const windowWidth = useWindowWidth();
|
const windowWidth = useWindowWidth();
|
||||||
const isMobileView = windowWidth <= 768;
|
const isMobileView = windowWidth <= 768;
|
||||||
|
// todo implement real read time needs to be on form
|
||||||
|
const readTime = 30;
|
||||||
|
|
||||||
|
const { isCompleted, isTracking } = useTrackDocumentLesson({
|
||||||
|
lessonId: lesson?.d,
|
||||||
|
courseId: course?.d,
|
||||||
|
readTime: readTime,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!zaps || zapsLoading || zapsError) return;
|
if (!zaps || zapsLoading || zapsError) return;
|
||||||
@ -45,6 +55,12 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
|
|||||||
}
|
}
|
||||||
}, [lesson]);
|
}, [lesson]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCompleted) {
|
||||||
|
setCompleted(lesson.id);
|
||||||
|
}
|
||||||
|
}, [isCompleted, lesson.id, setCompleted]);
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (isPaid && decryptionPerformed) {
|
if (isPaid && decryptionPerformed) {
|
||||||
return <MDDisplay className='p-2 rounded-lg w-full' source={lesson.content} />;
|
return <MDDisplay className='p-2 rounded-lg w-full' source={lesson.content} />;
|
||||||
@ -90,14 +106,14 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className='text-xl text-gray-200 mb-4 mt-4'>{lesson.summary && (
|
<div className='text-xl text-gray-200 mb-4 mt-4'>{lesson.summary && (
|
||||||
<div className="text-xl mt-4">
|
<div className="text-xl mt-4">
|
||||||
{lesson.summary.split('\n').map((line, index) => (
|
{lesson.summary.split('\n').map((line, index) => (
|
||||||
<p key={index}>{line}</p>
|
<p key={index}>{line}</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</p>
|
</div>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<Image
|
<Image
|
||||||
|
@ -292,11 +292,11 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons })
|
|||||||
['summary', draft.summary],
|
['summary', draft.summary],
|
||||||
['image', draft.image],
|
['image', draft.image],
|
||||||
// todo populate this tag from the config
|
// todo populate this tag from the config
|
||||||
['i', 'youtube:plebdevs', 'V_fvmyJ91m0'],
|
|
||||||
...draft.topics.map(topic => ['t', topic]),
|
...draft.topics.map(topic => ['t', topic]),
|
||||||
['published_at', Math.floor(Date.now() / 1000).toString()],
|
['published_at', Math.floor(Date.now() / 1000).toString()],
|
||||||
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
...(draft?.price ? [['price', draft.price.toString()], ['location', `https://plebdevs.com/details/${draft.id}`]] : []),
|
||||||
...(draft?.additionalLinks ? draft.additionalLinks.filter(link => link !== 'https://plebdevs.com').map(link => ['r', link]) : []),
|
...(draft?.additionalLinks ? draft.additionalLinks.filter(link => link !== 'https://plebdevs.com').map(link => ['r', link]) : []),
|
||||||
|
draft?.price ? null : ['i', 'youtube:plebdevs', 'V_fvmyJ91m0']
|
||||||
];
|
];
|
||||||
|
|
||||||
type = 'video';
|
type = 'video';
|
||||||
|
@ -38,17 +38,53 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
|
|||||||
videoPlayed
|
videoPlayed
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleYouTubeMessage = (event) => {
|
||||||
|
if (event.origin !== "https://www.youtube.com") return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
console.log('youtube data', data);
|
||||||
|
if (data.event === "onReady") {
|
||||||
|
event.source.postMessage('{"event":"listening"}', "https://www.youtube.com");
|
||||||
|
} else if (data.event === "infoDelivery" && data?.info && data?.info?.currentTime) {
|
||||||
|
setVideoPlayed(true);
|
||||||
|
setVideoDuration(data.info?.progressState?.duration);
|
||||||
|
event.source.postMessage('{"event":"command","func":"getDuration","args":""}', "https://www.youtube.com");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error parsing YouTube message:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("message", handleYouTubeMessage);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("message", handleYouTubeMessage);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (videoDuration && videoPlayed) {
|
||||||
|
console.log('videoDuration and videoPlayed', videoDuration, videoPlayed);
|
||||||
|
}
|
||||||
|
}, [videoDuration, videoPlayed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (videoPlayed) {
|
||||||
|
console.log('videoPlayed', videoPlayed);
|
||||||
|
}
|
||||||
|
}, [videoPlayed]);
|
||||||
|
|
||||||
const checkDuration = useCallback(() => {
|
const checkDuration = useCallback(() => {
|
||||||
const videoElement = mdDisplayRef.current?.querySelector('video');
|
const videoElement = mdDisplayRef.current?.querySelector('video');
|
||||||
|
const youtubeIframe = mdDisplayRef.current?.querySelector('iframe[src*="youtube.com"]');
|
||||||
|
|
||||||
if (videoElement && videoElement.readyState >= 1) {
|
if (videoElement && videoElement.readyState >= 1) {
|
||||||
setVideoDuration(Math.round(videoElement.duration));
|
setVideoDuration(Math.round(videoElement.duration));
|
||||||
|
|
||||||
// Add event listener for play event
|
|
||||||
videoElement.addEventListener('play', () => {
|
|
||||||
setVideoPlayed(true);
|
setVideoPlayed(true);
|
||||||
});
|
} else if (youtubeIframe) {
|
||||||
} else if (videoElement) {
|
youtubeIframe.contentWindow.postMessage('{"event":"listening"}', '*');
|
||||||
setTimeout(checkDuration, 100);
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -78,6 +114,10 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
|
|||||||
if (decryptionPerformed && isPaid) {
|
if (decryptionPerformed && isPaid) {
|
||||||
const timer = setTimeout(checkDuration, 500);
|
const timer = setTimeout(checkDuration, 500);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
|
} else {
|
||||||
|
// For non-paid content, start checking after 3 seconds
|
||||||
|
const timer = setTimeout(checkDuration, 3000);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [decryptionPerformed, isPaid, checkDuration]);
|
}, [decryptionPerformed, isPaid, checkDuration]);
|
||||||
|
|
||||||
@ -109,7 +149,11 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (lesson?.content) {
|
} else if (lesson?.content) {
|
||||||
return <MDDisplay className='p-0 rounded-lg w-full' source={lesson.content} />;
|
return (
|
||||||
|
<div ref={mdDisplayRef}>
|
||||||
|
<MDDisplay className='p-0 rounded-lg w-full' source={lesson.content} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -129,14 +173,14 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-row items-center justify-between w-full'>
|
<div className='flex flex-row items-center justify-between w-full'>
|
||||||
<p className='text-xl mt-4 text-gray-200'>{lesson.summary && (
|
<div className='text-xl mt-4 text-gray-200'>{lesson.summary && (
|
||||||
<div className="text-xl mt-4">
|
<div className="text-xl mt-4">
|
||||||
{lesson.summary.split('\n').map((line, index) => (
|
{lesson.summary.split('\n').map((line, index) => (
|
||||||
<p key={index}>{line}</p>
|
<p key={index}>{line}</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</p>
|
</div>
|
||||||
<ZapDisplay
|
<ZapDisplay
|
||||||
zapAmount={zapAmount}
|
zapAmount={zapAmount}
|
||||||
event={lesson}
|
event={lesson}
|
||||||
|
@ -12,6 +12,7 @@ import 'primeicons/primeicons.css';
|
|||||||
import { Tooltip } from 'primereact/tooltip';
|
import { Tooltip } from 'primereact/tooltip';
|
||||||
import 'primereact/resources/primereact.min.css';
|
import 'primereact/resources/primereact.min.css';
|
||||||
|
|
||||||
|
// todo need to handle case where published video is being edited and not just draft
|
||||||
const VideoForm = ({ draft = null }) => {
|
const VideoForm = ({ draft = null }) => {
|
||||||
const [title, setTitle] = useState(draft?.title || '');
|
const [title, setTitle] = useState(draft?.title || '');
|
||||||
const [summary, setSummary] = useState(draft?.summary || '');
|
const [summary, setSummary] = useState(draft?.summary || '');
|
||||||
@ -53,7 +54,7 @@ const VideoForm = ({ draft = null }) => {
|
|||||||
// Check if it's a YouTube video
|
// Check if it's a YouTube video
|
||||||
if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
|
if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
|
||||||
const videoId = videoUrl.split('v=')[1] || videoUrl.split('/').pop();
|
const videoId = videoUrl.split('v=')[1] || videoUrl.split('/').pop();
|
||||||
embedCode = `<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><iframe src="https://www.youtube.com/embed/${videoId}" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allowfullscreen></iframe></div>`;
|
embedCode = `<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><iframe src="https://www.youtube.com/embed/${videoId}?enablejsapi=1" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allowfullscreen></iframe></div>`;
|
||||||
}
|
}
|
||||||
// Check if it's a Vimeo video
|
// Check if it's a Vimeo video
|
||||||
else if (videoUrl.includes('vimeo.com')) {
|
else if (videoUrl.includes('vimeo.com')) {
|
||||||
|
@ -50,7 +50,7 @@ const EmbeddedVideoForm = ({ draft = null, onSave, isPaid }) => {
|
|||||||
// Check if it's a YouTube video
|
// Check if it's a YouTube video
|
||||||
if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
|
if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
|
||||||
const videoId = videoUrl.split('v=')[1] || videoUrl.split('/').pop();
|
const videoId = videoUrl.split('v=')[1] || videoUrl.split('/').pop();
|
||||||
embedCode = `<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><iframe src="https://www.youtube.com/embed/${videoId}" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allowfullscreen></iframe></div>`;
|
embedCode = `<div style="position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;"><iframe src="https://www.youtube.com/embed/${videoId}?enablejsapi=1" style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" allowfullscreen></iframe></div>`;
|
||||||
}
|
}
|
||||||
// Check if it's a Vimeo video
|
// Check if it's a Vimeo video
|
||||||
else if (videoUrl.includes('vimeo.com')) {
|
else if (videoUrl.includes('vimeo.com')) {
|
||||||
|
105
src/hooks/tracking/useTrackDocumentLesson.js
Normal file
105
src/hooks/tracking/useTrackDocumentLesson.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const useTrackDocumentLesson = ({ lessonId, courseId, readTime }) => {
|
||||||
|
const [isCompleted, setIsCompleted] = useState(false);
|
||||||
|
const [timeSpent, setTimeSpent] = useState(0);
|
||||||
|
const [isTracking, setIsTracking] = useState(false);
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
|
const timerRef = useRef(null);
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const completedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (session?.user?.role?.admin) {
|
||||||
|
setIsAdmin(true);
|
||||||
|
setIsCompleted(true); // Automatically mark as completed for admins
|
||||||
|
}
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
const checkOrCreateUserLesson = useCallback(async () => {
|
||||||
|
if (!session?.user) return false;
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/users/${session.user.id}/lessons/${lessonId}?courseId=${courseId}`);
|
||||||
|
if (response.status === 200 && response?.data) {
|
||||||
|
if (response?.data?.completed) {
|
||||||
|
setIsCompleted(true);
|
||||||
|
completedRef.current = true;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if (response.status === 204) {
|
||||||
|
await axios.post(`/api/users/${session.user.id}/lessons?courseId=${courseId}`, {
|
||||||
|
resourceId: lessonId,
|
||||||
|
opened: true,
|
||||||
|
openedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
console.error('Error checking or creating UserLesson:', response.statusText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking or creating UserLesson:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}, [session, lessonId, courseId]);
|
||||||
|
|
||||||
|
const markLessonAsCompleted = useCallback(async () => {
|
||||||
|
if (!session?.user || completedRef.current) return;
|
||||||
|
completedRef.current = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.put(`/api/users/${session.user.id}/lessons/${lessonId}?courseId=${courseId}`, {
|
||||||
|
completed: true,
|
||||||
|
completedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
setIsCompleted(true);
|
||||||
|
setIsTracking(false);
|
||||||
|
} else {
|
||||||
|
console.error('Failed to mark lesson as completed:', response.statusText);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error marking lesson as completed:', error);
|
||||||
|
}
|
||||||
|
}, [lessonId, courseId, session]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initializeTracking = async () => {
|
||||||
|
if (isAdmin) return; // Skip tracking for admin users
|
||||||
|
|
||||||
|
const alreadyCompleted = await checkOrCreateUserLesson();
|
||||||
|
if (!alreadyCompleted && !completedRef.current) {
|
||||||
|
setIsTracking(true);
|
||||||
|
timerRef.current = setInterval(() => {
|
||||||
|
setTimeSpent(prevTime => prevTime + 1);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeTracking();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [lessonId, checkOrCreateUserLesson, isAdmin]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAdmin) return; // Skip tracking for admin users
|
||||||
|
|
||||||
|
// Mark lesson as completed after readTime seconds
|
||||||
|
if (timeSpent >= readTime && !completedRef.current) {
|
||||||
|
markLessonAsCompleted();
|
||||||
|
}
|
||||||
|
}, [timeSpent, markLessonAsCompleted, readTime, isAdmin]);
|
||||||
|
|
||||||
|
return { isCompleted, isTracking };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTrackDocumentLesson;
|
@ -6,10 +6,18 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
|
|||||||
const [isCompleted, setIsCompleted] = useState(false);
|
const [isCompleted, setIsCompleted] = useState(false);
|
||||||
const [timeSpent, setTimeSpent] = useState(0);
|
const [timeSpent, setTimeSpent] = useState(0);
|
||||||
const [isTracking, setIsTracking] = useState(false);
|
const [isTracking, setIsTracking] = useState(false);
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
const timerRef = useRef(null);
|
const timerRef = useRef(null);
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const completedRef = useRef(false);
|
const completedRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (session?.user?.role?.admin) {
|
||||||
|
setIsAdmin(true);
|
||||||
|
setIsCompleted(true); // Automatically mark as completed for admins
|
||||||
|
}
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
// Check if the lesson is already completed or create a new UserLesson record
|
// Check if the lesson is already completed or create a new UserLesson record
|
||||||
const checkOrCreateUserLesson = useCallback(async () => {
|
const checkOrCreateUserLesson = useCallback(async () => {
|
||||||
if (!session?.user) return false;
|
if (!session?.user) return false;
|
||||||
@ -68,6 +76,9 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeTracking = async () => {
|
const initializeTracking = async () => {
|
||||||
|
console.log('initializeTracking', videoDuration, !completedRef.current, videoPlayed);
|
||||||
|
if (isAdmin) return; // Skip tracking for admin users
|
||||||
|
|
||||||
const alreadyCompleted = await checkOrCreateUserLesson();
|
const alreadyCompleted = await checkOrCreateUserLesson();
|
||||||
// Case 3: Start tracking if the lesson is not completed, video duration is available, and video has been played
|
// Case 3: Start tracking if the lesson is not completed, video duration is available, and video has been played
|
||||||
if (!alreadyCompleted && videoDuration && !completedRef.current && videoPlayed) {
|
if (!alreadyCompleted && videoDuration && !completedRef.current && videoPlayed) {
|
||||||
@ -87,14 +98,16 @@ const useTrackVideoLesson = ({lessonId, videoDuration, courseId, videoPlayed}) =
|
|||||||
clearInterval(timerRef.current);
|
clearInterval(timerRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [lessonId, videoDuration, checkOrCreateUserLesson, videoPlayed]);
|
}, [lessonId, videoDuration, checkOrCreateUserLesson, videoPlayed, isAdmin]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isAdmin) return; // Skip tracking for admin users
|
||||||
|
|
||||||
// Case 4: Mark lesson as completed when 90% of the video is watched
|
// Case 4: Mark lesson as completed when 90% of the video is watched
|
||||||
if (videoDuration && timeSpent >= Math.round(videoDuration * 0.9) && !completedRef.current) {
|
if (videoDuration && timeSpent >= Math.round(videoDuration * 0.9) && !completedRef.current) {
|
||||||
markLessonAsCompleted();
|
markLessonAsCompleted();
|
||||||
}
|
}
|
||||||
}, [timeSpent, videoDuration, markLessonAsCompleted]);
|
}, [timeSpent, videoDuration, markLessonAsCompleted, isAdmin]);
|
||||||
|
|
||||||
return { isCompleted, isTracking };
|
return { isCompleted, isTracking };
|
||||||
};
|
};
|
||||||
|
@ -106,6 +106,7 @@ export const authOptions = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo this does not work on first login only the second time
|
||||||
if (user && appConfig.authorPubkeys.includes(user?.pubkey) && !user?.role) {
|
if (user && appConfig.authorPubkeys.includes(user?.pubkey) && !user?.role) {
|
||||||
// create a new author role for this user
|
// create a new author role for this user
|
||||||
const role = await createRole({
|
const role = await createRole({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user