Skip to content

Commit

Permalink
Merge pull request #253 from nimit9/feat/user-watch-history
Browse files Browse the repository at this point in the history
feat: users can now see their watch history
  • Loading branch information
hkirat authored Apr 1, 2024
2 parents f76479a + d518045 commit ea9d635
Show file tree
Hide file tree
Showing 17 changed files with 567 additions and 155 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"discord-oauth2": "^2.11.0",
"discord.js": "^14.14.1",
"fuse.js": "^7.0.0",
"embla-carousel-react": "^8.0.0",
"jose": "^5.2.2",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.321.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "VideoProgress" ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
11 changes: 6 additions & 5 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ model User {
bookmarks Bookmark[]
password String?
appxUserId String?
appxUsername String?
appxUsername String?
}

model DiscordConnect {
Expand All @@ -150,13 +150,14 @@ model DiscordConnectBulk {
}

model VideoProgress {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
userId String
contentId Int
currentTimestamp Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
content Content @relation(fields: [contentId], references: [id], onDelete: Cascade)
markAsCompleted Boolean @default(false)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
content Content @relation(fields: [contentId], references: [id], onDelete: Cascade)
markAsCompleted Boolean @default(false)
updatedAt DateTime @default(now()) @updatedAt
@@unique([contentId, userId])
}
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/course/videoProgress/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import db from '@/db';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { revalidatePath } from 'next/cache';

export async function GET(req: NextRequest) {
const url = new URL(req.url);
Expand Down Expand Up @@ -47,5 +48,6 @@ export async function POST(req: NextRequest) {
currentTimestamp,
},
});
revalidatePath('/history');
return NextResponse.json(updatedRecord);
}
11 changes: 11 additions & 0 deletions src/app/history/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { CourseSkeleton } from '@/components/CourseCard';

export default function Loading() {
return (
<div className="max-w-screen-xl justify-between mx-auto p-4 cursor-pointer grid grid-cols-1 gap-5 md:grid-cols-3">
{[1, 2, 3].map((v) => (
<CourseSkeleton key={v} />
))}
</div>
);
}
107 changes: 107 additions & 0 deletions src/app/history/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import db from '@/db';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { Content, CourseContent, VideoProgress } from '@prisma/client';
import WatchHistoryClient from '@/components/WatchHistoryClient';
import { Fragment } from 'react';

export type TWatchHistory = VideoProgress & {
content: Content & {
parent: { id: number; courses: CourseContent[] } | null;
VideoMetadata: { duration: number | null } | null;
};
};

const formatWatchHistoryDate = (date: Date) => {
const now = new Date();
const diff = now.getTime() - date.getTime();
const diffInDays = diff / (1000 * 60 * 60 * 24);

if (diffInDays < 1) {
return 'Today';
} else if (diffInDays < 2) {
return 'Yesterday';
}
const currentYear = now.getFullYear();

const historyYear = date.getFullYear();

if (currentYear - historyYear > 0) {
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
};

const groupByWatchedDate = (userVideoProgress: TWatchHistory[]) => {
return userVideoProgress.reduce(
(acc, item) => {
const date = new Date(item.updatedAt);
const formattedDate = formatWatchHistoryDate(date);

if (!acc[formattedDate]) {
acc[formattedDate] = [];
}
acc[formattedDate].push(item);
return acc;
},
{} as { [key: string]: TWatchHistory[] },
);
};

async function getWatchHistory() {
const session = await getServerSession(authOptions);
const userId = session.user.id;

const userVideoProgress: TWatchHistory[] = await db.videoProgress.findMany({
where: {
userId,
},
include: {
content: {
include: {
VideoMetadata: {
select: {
duration: true,
},
},
parent: {
select: {
id: true,
courses: true,
},
},
},
},
},
orderBy: {
updatedAt: 'desc',
},
});

return userVideoProgress;
}

export default async function CoursesComponent() {
const watchHistory = await getWatchHistory();
const watchHistoryGroupedByDate = groupByWatchedDate(watchHistory);

return (
<div className="px-4 md:px-20 py-5">
<h1 className="text-2xl font-bold">Watch history</h1>
<div className="flex flex-col gap-4 my-4 sm:my-8 max-w-full">
{Object.entries(watchHistoryGroupedByDate).map(([date, history]) => {
return (
<Fragment key={date}>
<h2 className="text-lg font-semibold mt-4 sm:mt-0">{date}</h2>
<WatchHistoryClient history={history} />
</Fragment>
);
})}
</div>
</div>
);
}
91 changes: 46 additions & 45 deletions src/components/Appbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,56 +48,57 @@ export const Appbar = () => {
<div className="flex items-center space-x-2">
{/* Search Bar for smaller devices */}
<MobileScreenSearch />
<div className="hidden sm:flex items-center justify-around md:w-auto md:block space-x-2">
<Button variant={'link'} size={'sm'} asChild>
<JoinDiscord isNavigated={false} />
</Button>
<div className="flex items-center space-x-2">
<div className="hidden sm:flex items-center justify-around md:w-auto md:block space-x-2">
{currentPath.includes('courses') && bookmarkPageUrl && (
<Button
variant="link"
className={
currentPath === bookmarkPageUrl
? 'font-bold underline'
: ''
}
size={'sm'}
asChild
>
<Link href={bookmarkPageUrl}>Bookmarks</Link>
</Button>
)}
<div className="flex items-center space-x-2">
<div className="hidden sm:flex items-center justify-around md:w-auto md:block space-x-2">
{currentPath.includes('courses') && bookmarkPageUrl && (
<Button
variant="link"
className={
currentPath === bookmarkPageUrl
? 'font-bold underline'
: ''
}
size={'sm'}
asChild
>
<Link href={bookmarkPageUrl}>Bookmarks</Link>
</Button>
)}

<Button variant={'link'} size={'sm'} asChild>
<JoinDiscord isNavigated={false} />
</Button>
<Button variant={'link'} size={'sm'} asChild>
<JoinDiscord isNavigated={false} />
</Button>

<Button size={'sm'} variant={'link'} asChild>
<Link
href={'https://projects.100xdevs.com/'}
target="_blank"
>
Slides
</Link>
</Button>
<Button size={'sm'} variant={'link'} asChild>
<Link
href={'https://projects.100xdevs.com/'}
target="_blank"
>
Slides
</Link>
</Button>

<Button size={'sm'} variant={'link'} asChild>
<Link
href={'https://github.com/100xdevs-cohort-2/assignments'}
target="_blank"
>
Assignments
</Link>
</Button>

<AppbarAuth />
</div>
<Button size={'sm'} variant={'link'} asChild>
<Link
href={
'https://github.com/100xdevs-cohort-2/assignments'
}
target="_blank"
>
Assignments
</Link>
</Button>
<Button size={'sm'} variant={'link'} asChild>
<Link href={'/history'}>Watch History</Link>
</Button>
<AppbarAuth />
</div>

<ThemeToggler />
<ThemeToggler />

<div className="block sm:hidden">
<NavigationMenu />
<div className="block sm:hidden">
<NavigationMenu />
</div>
</div>
</div>
</>
Expand Down
19 changes: 16 additions & 3 deletions src/components/ContentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const ContentCard = ({
markAsCompleted,
percentComplete,
type,
videoProgressPercent,
hoverExpand = true,
bookmark,
contentId,
}: {
Expand All @@ -19,6 +21,8 @@ export const ContentCard = ({
onClick: () => void;
markAsCompleted?: boolean;
percentComplete?: number | null;
videoProgressPercent?: number;
hoverExpand?: boolean;
bookmark?: Bookmark | null;
}) => {
let image =
Expand All @@ -31,7 +35,7 @@ export const ContentCard = ({
return (
<div
onClick={onClick}
className="relative hover:scale-105 ease-in duration-200 group"
className={`relative ease-in duration-200 cursor-pointer group${hoverExpand ? ' hover:scale-105' : ''} `}
>
{percentComplete !== null && percentComplete !== undefined && (
<PercentageComplete percent={percentComplete} />
Expand All @@ -41,6 +45,17 @@ export const ContentCard = ({
<CheckCircle2 color="green" size={20} />
</div>
)}
<div className="relative overflow-hidden rounded-md">
<img src={image} alt={title} className="" />
{!!videoProgressPercent && (
<div className="absolute bottom-0 w-full h-1 bg-[#707071]">
<div
className="h-full bg-[#FF0101]"
style={{ width: `${videoProgressPercent}%` }}
/>
</div>
)}
</div>
{bookmark !== undefined && contentId && (
<div className="absolute top-2 left-2">
<BookmarkButton
Expand All @@ -52,8 +67,6 @@ export const ContentCard = ({
/>
</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>
</div>
Expand Down
2 changes: 2 additions & 0 deletions src/components/CourseView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export const CourseView = ({
id: x?.id || 0,
markAsCompleted: x?.videoProgress?.markAsCompleted || false,
percentComplete: getFolderPercentCompleted(x?.children),
videoFullDuration: x?.videoProgress?.videoFullDuration || 0,
duration: x?.videoProgress?.duration || 0,
}))}
courseId={parseInt(course.id, 10)}
/>
Expand Down
39 changes: 25 additions & 14 deletions src/components/FolderView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export const FolderView = ({
id: number;
markAsCompleted: boolean;
percentComplete: number | null;
videoFullDuration?: number;
duration?: number;
bookmark: Bookmark | null;
}[];
}) => {
Expand All @@ -39,20 +41,29 @@ 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) => (
<ContentCard
type={content.type}
key={content.id}
title={content.title}
image={content.image || ''}
onClick={() => {
router.push(`${updatedRoute}/${content.id}`);
}}
markAsCompleted={content.markAsCompleted}
percentComplete={content.percentComplete}
bookmark={content.bookmark}
/>
))}
{courseContent.map((content) => {
const videoProgressPercent =
content.type === 'video' &&
content.videoFullDuration &&
content.duration
? (content.duration / content.videoFullDuration) * 100
: 0;
return (
<ContentCard
type={content.type}
key={content.id}
title={content.title}
image={content.image || ''}
onClick={() => {
router.push(`${updatedRoute}/${content.id}`);
}}
markAsCompleted={content.markAsCompleted}
percentComplete={content.percentComplete}
videoProgressPercent={videoProgressPercent}
bookmark={content.bookmark}
/>
);
})}
</div>
</div>
);
Expand Down
Loading

0 comments on commit ea9d635

Please sign in to comment.