Skip to content

Commit

Permalink
Merge pull request #64 from nimit9/feat/user-progress
Browse files Browse the repository at this point in the history
Feat/user progress
  • Loading branch information
hkirat authored Feb 4, 2024
2 parents 72516ac + f8082e6 commit d70659f
Show file tree
Hide file tree
Showing 20 changed files with 275 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "VideoProgress" ADD COLUMN "markAsCompleted" BOOLEAN NOT NULL DEFAULT false;
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ model VideoProgress {
currentTimestamp Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
content Content @relation(fields: [contentId], references: [id], onDelete: Cascade)
markAsCompleted Boolean @default(false)
@@unique([contentId, userId])
}
32 changes: 32 additions & 0 deletions src/app/api/course/videoProgress/markAsCompleted/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from 'next/server';
import db from '@/db';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';

export async function POST(req: NextRequest) {
const { contentId, markAsCompleted } = await req.json();
const session = await getServerSession(authOptions);
if (!session || !session?.user) {
return NextResponse.json({}, { status: 401 });
}

const updatedRecord = await db.videoProgress.upsert({
where: {
contentId_userId: {
contentId: Number(contentId),
userId: session.user.id,
},
},
create: {
contentId: Number(contentId),
userId: session.user.id,
currentTimestamp: 0,
markAsCompleted,
},
update: {
markAsCompleted,
},
});

return NextResponse.json(updatedRecord);
}
24 changes: 20 additions & 4 deletions src/app/api/course/videoProgress/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,31 @@ export async function GET(req: NextRequest) {
});
return NextResponse.json({
progress: currentProgress?.currentTimestamp ?? 0,
markAsCompleted: currentProgress?.markAsCompleted ?? false,
});
}

export async function POST() {
export async function POST(req: NextRequest) {
const { contentId, currentTimestamp } = await req.json();
const session = await getServerSession(authOptions);

if (!session || !session?.user) {
return NextResponse.json({}, { status: 401 });
}

return NextResponse.json({});
const updatedRecord = await db.videoProgress.upsert({
where: {
contentId_userId: {
contentId: Number(contentId),
userId: session.user.id,
},
},
create: {
contentId: Number(contentId),
userId: session.user.id,
currentTimestamp,
},
update: {
currentTimestamp,
},
});
return NextResponse.json(updatedRecord);
}
21 changes: 20 additions & 1 deletion src/components/ContentCard.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,33 @@
import { CheckCircle2 } from 'lucide-react';
import PercentageComplete from './PercentageComplete';

export const ContentCard = ({
image,
title,
onClick,
markAsCompleted,
percentComplete,
}: {
contentId?: number
image: string
title: string
onClick: () => void
markAsCompleted?: boolean
percentComplete?: number | null
}) => {
return (
<div onClick={onClick} className="hover:scale-105 ease-in duration-200">
<div
onClick={onClick}
className="relative hover:scale-105 ease-in duration-200"
>
{percentComplete !== null && percentComplete !== undefined && (
<PercentageComplete percent={percentComplete} />
)}
{markAsCompleted && (
<div className="absolute top-2 right-2">
<CheckCircle2 color="green" size={20} />
</div>
)}
<img src={image} alt={title} className="rounded-md" />
<div className="flex justify-between mt-2 text-gray-900 dark:text-white">
<div>{title}</div>
Expand Down
12 changes: 12 additions & 0 deletions src/components/CourseCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';
import { Course } from '@/store/atoms';
import PercentageComplete from './PercentageComplete';
import { Button } from './ui/button';
import { ChevronRight } from 'lucide-react';

Expand All @@ -17,6 +18,17 @@ export const CourseCard = ({
onClick();
}}
>
<div className="relative">
{course.totalVideos !== undefined &&
course.totalVideosWatched !== undefined && (
<PercentageComplete
percent={Math.ceil(
(course.totalVideosWatched / course.totalVideos) * 100,
)}
/>
)}
<img src={course.imageUrl} alt={course.title} className="rounded-md" />
</div>
<div className="p-2">
<img
src={course.imageUrl}
Expand Down
7 changes: 7 additions & 0 deletions src/components/CourseView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ContentRenderer } from './admin/ContentRenderer';
import { FolderView } from './FolderView';
import { Sidebar } from './Sidebar';
import { NotionRenderer } from './NotionRenderer';
import { getFolderPercentCompleted } from '@/lib/utils';

export const CourseView = ({
rest,
Expand Down Expand Up @@ -37,6 +38,9 @@ export const CourseView = ({
title: courseContent[0]?.title || '',
type: contentType || 'video',
description: courseContent[0]?.description || '',
markAsCompleted:
courseContent[0]?.videoProgress[0]?.markAsCompleted ||
false,
}}
/>
) : null}
Expand All @@ -47,6 +51,9 @@ export const CourseView = ({
title: x?.title || '',
image: x?.thumbnail || '',
id: x?.id || 0,
markAsCompleted:
x?.videoProgress[0]?.markAsCompleted || false,
percentComplete: getFolderPercentCompleted(x?.children),
}))}
courseId={parseInt(course.id, 10)}
/>
Expand Down
26 changes: 14 additions & 12 deletions src/components/FolderView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const FolderView = ({
title: string
image: string
id: number
markAsCompleted: boolean
percentComplete: number | null
}[]
}) => {
const router = useRouter();
Expand All @@ -34,18 +36,18 @@ export const FolderView = ({
<div>
<div></div>
<div className="max-w-screen-xl justify-between mx-auto p-4 cursor-pointer grid grid-cols-1 gap-5 md:grid-cols-3">
{courseContent.map(
(content: { image: string; id: number; title: string }) => (
<ContentCard
key={content.id}
title={content.title}
image={content.image || ''}
onClick={() => {
router.push(`${updatedRoute}/${content.id}`);
}}
/>
),
)}
{courseContent.map((content) => (
<ContentCard
key={content.id}
title={content.title}
image={content.image || ''}
onClick={() => {
router.push(`${updatedRoute}/${content.id}`);
}}
markAsCompleted={content.markAsCompleted}
percentComplete={content.percentComplete}
/>
))}
</div>
</div>
);
Expand Down
11 changes: 11 additions & 0 deletions src/components/PercentageComplete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';

const PercentageComplete = ({ percent }: { percent: number }) => {
return (
<div className="text-xs absolute top-2 right-2 text-[#1D4ED8] border-[#1D4ED8] border py-1 px-2 rounded-full font-bold">
<div>{`${percent}% completed`}</div>
</div>
);
};

export default PercentageComplete;
2 changes: 1 addition & 1 deletion src/components/VideoContentChapters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const VideoContentChapters = ({
<X onClick={onCancel} className="cursor-pointer" />
</div>
<div className="max-h-[70vh] overflow-auto">
{(segments as Segment[]).map(({ start, end, title }, index) => {
{(segments as Segment[])?.map(({ start, end, title }, index) => {
return (
<>
<div
Expand Down
52 changes: 39 additions & 13 deletions src/components/VideoPlayer2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import 'videojs-contrib-eme';
import 'videojs-mobile-ui/dist/videojs-mobile-ui.css';
import 'videojs-mobile-ui';
import 'videojs-sprite-thumbnails';
import { handleMarkAsCompleted } from '@/lib/utils';

// todo correct types
interface VideoPlayerProps {
options: any
onReady?: (player: Player) => void
subtitles?: string
contentId: number
onVideoEnd: () => void
}

const PLAYBACK_RATES: number[] = [0.5, 1, 1.25, 1.5, 1.75, 2];
Expand All @@ -24,6 +26,7 @@ export const VideoPlayer: FunctionComponent<VideoPlayerProps> = ({
contentId,
onReady,
subtitles,
onVideoEnd,
}) => {
const videoRef = useRef<HTMLDivElement>(null);
const playerRef = useRef<Player | null>(null);
Expand Down Expand Up @@ -140,23 +143,46 @@ export const VideoPlayer: FunctionComponent<VideoPlayerProps> = ({
}, [player]);

useEffect(() => {
const interval = window.setInterval(async () => {
const currentTime = player.currentTime();
if (currentTime <= 20) {
if (!player) {
return;
}
let interval = 0;

const handleVideoProgress = () => {
if (!player) {
return;
}
await fetch('/api/course/videoProgress', {
body: JSON.stringify({
currentTimestamp: currentTime,
contentId,
}),
method: 'POST',
headers: {
'Content-Type': 'application/json',
interval = window.setInterval(
async () => {
if (player?.paused()) {
return;
}
const currentTime = player.currentTime();
if (currentTime <= 20) {
return;
}
await fetch('/api/course/videoProgress', {
body: JSON.stringify({
currentTimestamp: currentTime,
contentId,
}),
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
});
},
});
}, 10 * 1000);
Math.ceil((10 * 1000) / player.playbackRate()),
);
};
const handleVideoEnded = (interval: number) => {
handleMarkAsCompleted(true, contentId);
window.clearInterval(interval);
onVideoEnd();
};

player.on('play', handleVideoProgress);
player.on('ended', () => handleVideoEnded(interval));
return () => {
window.clearInterval(interval);
};
Expand Down
3 changes: 3 additions & 0 deletions src/components/VideoPlayerSegment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ interface VideoProps {
subtitles: string
videoJsOptions: any
contentId: number
onVideoEnd: () => void
}

export const VideoPlayerSegment: FunctionComponent<VideoProps> = ({
contentId,
subtitles,
segments,
videoJsOptions,
onVideoEnd,
}) => {
const playerRef = useRef<Player | null>(null);
const thumbnailPreviewRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -95,6 +97,7 @@ export const VideoPlayerSegment: FunctionComponent<VideoProps> = ({
subtitles={subtitles}
options={videoJsOptions}
onReady={handlePlayerReady}
onVideoEnd={onVideoEnd}
/>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions src/components/admin/ContentRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const ContentRenderer = async ({
description: string
thumbnail: string
slides?: string
markAsCompleted: boolean
}
}) => {
const metadata = await getMetadata(content.id);
Expand Down
Loading

0 comments on commit d70659f

Please sign in to comment.