Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: users can now see their watch history #253

Merged
merged 8 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading