Skip to content

Commit 99dd0fa

Browse files
feat:Added Filter Button to filter content by watched, watching & unwatched videos and optimised Admin Panel (#1486)
* feat:Added Filter Button to filter out watched, watching & unwatched content * feat:Added Filter Button and enhanced admin panel * few fixes * final fixes to the new features * final fixes to the new features --------- Co-authored-by: Sargam <[email protected]>
1 parent f2596bb commit 99dd0fa

File tree

15 files changed

+369
-75
lines changed

15 files changed

+369
-75
lines changed

README.md

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,11 @@ chmod +x setup.sh
3939

4040
```bash
4141
docker run -d \
42-
4342
--name cms-db \
44-
45-
-e POSTGRES_USER=myuser \
46-
43+
-e POSTGRES_USER=myuser \
4744
-e POSTGRES_PASSWORD=mypassword \
48-
49-
-e POSTGRES_DB=mydatabase \
50-
45+
-e POSTGRES_DB=mydatabase \
5146
-p 5432:5432 \
52-
5347
postgres
5448
```
5549

@@ -69,7 +63,7 @@ pnpm install
6963
3. Run database migrations:
7064

7165
```bash
72-
pnpm run prisma:migrate
66+
pnpm prisma:migrate
7367
```
7468

7569
4. Generate prisma client
@@ -81,13 +75,13 @@ pnpm prisma generate
8175
5. Seed the database:
8276

8377
```bash
84-
pnpm run db:seed
78+
pnpm db:seed
8579
```
8680

8781
6. Start the development server:
8882

8983
```bash
90-
pnpm run dev
84+
pnpm dev
9185
```
9286

9387
## Usage
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import db from '@/db';
3+
import { getServerSession } from 'next-auth';
4+
import { authOptions } from '@/lib/auth';
5+
import { z } from 'zod';
6+
7+
const requestBodySchema = z.object({
8+
contentId: z.number(),
9+
duration: z.number(),
10+
});
11+
12+
export async function POST(req: NextRequest) {
13+
const parseResult = requestBodySchema.safeParse(await req.json());
14+
15+
if (!parseResult.success) {
16+
return NextResponse.json(
17+
{ error: parseResult.error.message },
18+
{ status: 400 },
19+
);
20+
}
21+
const { contentId, duration } = parseResult.data;
22+
const session = await getServerSession(authOptions);
23+
if (!session || !session?.user) {
24+
return NextResponse.json({}, { status: 401 });
25+
}
26+
27+
const updatedRecord = await db.videoMetadata.upsert({
28+
where: {
29+
contentId: Number(contentId),
30+
},
31+
create: {
32+
contentId: Number(contentId),
33+
duration: Number(duration),
34+
},
35+
update: {
36+
duration,
37+
},
38+
});
39+
40+
return NextResponse.json(updatedRecord);
41+
}

src/app/courses/[courseId]/layout.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { QueryParams } from '@/actions/types';
2+
import { FilterContent } from '@/components/FilterContent';
23
import { Sidebar } from '@/components/Sidebar';
34
import { getFullCourseContent } from '@/db/course';
45
import { authOptions } from '@/lib/auth';
@@ -46,10 +47,17 @@ const Layout = async ({
4647
}
4748

4849
const fullCourseContent = await getFullCourseContent(parseInt(courseId, 10));
49-
5050
return (
5151
<div className="relative flex min-h-screen flex-col py-24">
52-
<Sidebar fullCourseContent={fullCourseContent} courseId={courseId} />
52+
<div className="flex justify-between">
53+
<div className="2/3">
54+
<Sidebar fullCourseContent={fullCourseContent} courseId={courseId} />
55+
</div>
56+
<div className="w-1/3">
57+
<FilterContent />
58+
</div>
59+
</div>
60+
5361
{children}
5462
</div>
5563
);

src/components/ContentCard.tsx

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import { formatTime } from '@/lib/utils';
55
import VideoThumbnail from './videothumbnail';
66
import CardComponent from './CardComponent';
77
import { motion } from 'framer-motion';
8-
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
8+
import {
9+
Tooltip,
10+
TooltipContent,
11+
TooltipProvider,
12+
TooltipTrigger,
13+
} from './ui/tooltip';
914
import React from 'react';
1015

1116
export const ContentCard = ({
@@ -40,7 +45,9 @@ export const ContentCard = ({
4045
onClick={onClick}
4146
tabIndex={0}
4247
role="button"
43-
onKeyDown={(e: React.KeyboardEvent) => (['Enter', ' '].includes(e.key) && onClick())}
48+
onKeyDown={(e: React.KeyboardEvent) =>
49+
['Enter', ' '].includes(e.key) && onClick()
50+
}
4451
className={`group relative flex h-fit w-full max-w-md cursor-pointer flex-col gap-2 rounded-2xl transition-all duration-300 hover:-translate-y-2`}
4552
>
4653
{markAsCompleted && (
@@ -57,7 +64,9 @@ export const ContentCard = ({
5764
<div className="relative overflow-hidden rounded-md">
5865
<CardComponent
5966
title={title}
60-
contentDuration={contentDuration && formatTime(contentDuration)}
67+
contentDuration={
68+
contentDuration && formatTime(contentDuration)
69+
}
6170
type={type}
6271
/>
6372
{!!videoProgressPercent && (
@@ -76,10 +85,18 @@ export const ContentCard = ({
7685
title={title}
7786
contentId={contentId ?? 0}
7887
imageUrl=""
79-
// imageUrl={
80-
// 'https://d2szwvl7yo497w.cloudfront.net/courseThumbnails/video.png'
81-
// }
88+
// imageUrl={
89+
// 'https://d2szwvl7yo497w.cloudfront.net/courseThumbnails/video.png'
90+
// }
8291
/>
92+
{!!videoProgressPercent && (
93+
<div className="absolute bottom-0 h-1 w-full bg-[#707071]">
94+
<div
95+
className="h-full bg-[#5eff01]"
96+
style={{ width: `${videoProgressPercent}%` }}
97+
></div>
98+
</div>
99+
)}
83100
</div>
84101
)}
85102
<div className="flex items-center justify-between gap-4">
@@ -98,11 +115,12 @@ export const ContentCard = ({
98115
</div>
99116
</motion.div>
100117
</TooltipTrigger>
101-
{
102-
Array.isArray(weeklyContentTitles) && weeklyContentTitles?.length > 0 && <TooltipContent sideOffset={16}>
103-
{weeklyContentTitles?.map((title) => <p>{title}</p>)}
104-
</TooltipContent>
105-
}
118+
{Array.isArray(weeklyContentTitles) &&
119+
weeklyContentTitles?.length > 0 && (
120+
<TooltipContent sideOffset={16}>
121+
{weeklyContentTitles?.map((title) => <p>{title}</p>)}
122+
</TooltipContent>
123+
)}
106124
</Tooltip>
107125
</TooltipProvider>
108126
);

src/components/CourseView.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ export const CourseView = ({
5151
</div>
5252

5353
{!courseContent?.folder && courseContent?.value.type === 'notion' ? (
54-
<NotionRenderer id={courseContent?.value?.id?.toString()} />
54+
<NotionRenderer
55+
id={courseContent?.value?.id?.toString()}
56+
courseId={courseContent.value.id}
57+
/>
5558
) : null}
56-
5759
{!courseContent?.folder && (contentType === 'video' || contentType === 'appx') ? (
5860
<ContentRenderer
5961
nextContent={nextContent}
@@ -70,7 +72,6 @@ export const CourseView = ({
7072
}}
7173
/>
7274
) : null}
73-
7475
{!courseContent?.folder &&
7576
(contentType === 'video' || contentType === 'notion') && (
7677
<Comments
@@ -84,7 +85,6 @@ export const CourseView = ({
8485
searchParams={searchParams}
8586
/>
8687
)}
87-
8888
{courseContent?.folder ? (
8989
<FolderView
9090
rest={rest}

src/components/FilterContent.tsx

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use client';
2+
import React, { useState, forwardRef } from 'react';
3+
import { Check, ChevronsUpDown } from 'lucide-react';
4+
import { cn } from '@/lib/utils';
5+
import { Button } from '@/components/ui/button';
6+
import { useRecoilState } from 'recoil';
7+
import { selectFilter } from '@/store/atoms/filterContent';
8+
import {
9+
Command,
10+
CommandGroup,
11+
CommandItem,
12+
CommandList,
13+
} from '@/components/ui/command';
14+
import {
15+
Popover,
16+
PopoverContent,
17+
PopoverTrigger,
18+
} from '@/components/ui/popover';
19+
20+
const allFilters = [
21+
{ value: 'all', label: 'ALL' },
22+
{ value: 'unwatched', label: 'Unwatched' },
23+
{ value: 'watched', label: 'Watched' },
24+
{ value: 'watching', label: 'Watching' },
25+
];
26+
27+
type FilterContentProps = {
28+
// Add any other props here if needed
29+
className?: string;
30+
};
31+
32+
export const FilterContent = forwardRef<HTMLDivElement, FilterContentProps>(
33+
(props, ref) => {
34+
const [open, setOpen] = useState(false);
35+
const [value, setValue] = useRecoilState(selectFilter);
36+
37+
return (
38+
<Popover open={open} onOpenChange={setOpen}>
39+
<PopoverTrigger asChild>
40+
<Button
41+
variant="outline"
42+
role="combobox"
43+
aria-expanded={open}
44+
className={`w-fit gap-2 ${props.className || ''}`}
45+
>
46+
{value
47+
? allFilters.find((filters) => filters.value === value)?.label
48+
: 'Filter'}
49+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
50+
</Button>
51+
</PopoverTrigger>
52+
<PopoverContent className="z-[99999] w-fit p-0" ref={ref}>
53+
<Command>
54+
<CommandList>
55+
<CommandGroup>
56+
{allFilters.map((filters) => (
57+
<CommandItem
58+
key={filters.value}
59+
value={filters.value}
60+
className={`px-4 ${props.className || ''}`}
61+
onSelect={(currentValue) => {
62+
setValue(currentValue === value ? '' : currentValue);
63+
setOpen(false);
64+
}}
65+
>
66+
<Check
67+
className={cn(
68+
'mr-2 h-4 w-4',
69+
value === filters.value ? 'opacity-100' : 'opacity-0',
70+
)}
71+
/>
72+
{filters.label}
73+
</CommandItem>
74+
))}
75+
</CommandGroup>
76+
</CommandList>
77+
</Command>
78+
</PopoverContent>
79+
</Popover>
80+
);
81+
},
82+
);

src/components/FolderView.tsx

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
'use client';
22
import { useRouter } from 'next/navigation';
33
import { ContentCard } from './ContentCard';
4-
import { Bookmark } from '@prisma/client';
5-
import { CourseContentType } from '@/lib/utils';
4+
import { courseContent, getFilteredContent } from '@/lib/utils';
5+
import { useRecoilValue } from 'recoil';
6+
import { selectFilter } from '@/store/atoms/filterContent';
67

78
export const FolderView = ({
89
courseContent,
@@ -11,18 +12,7 @@ export const FolderView = ({
1112
}: {
1213
courseId: number;
1314
rest: string[];
14-
courseContent: {
15-
type: CourseContentType;
16-
title: string;
17-
image: string;
18-
id: number;
19-
markAsCompleted: boolean;
20-
percentComplete: number | null;
21-
videoFullDuration?: number;
22-
duration?: number;
23-
bookmark: Bookmark | null;
24-
weeklyContentTitles?: string[];
25-
}[];
15+
courseContent: courseContent[];
2616
}) => {
2717
const router = useRouter();
2818

@@ -39,16 +29,24 @@ export const FolderView = ({
3929
}
4030
// why? because we have to reset the segments or they will be visible always after a video
4131

32+
const currentfilter = useRecoilValue(selectFilter);
33+
34+
const filteredCourseContent = getFilteredContent(
35+
courseContent,
36+
currentfilter,
37+
);
38+
4239
return (
4340
<div>
4441
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
45-
{courseContent.map((content) => {
42+
{filteredCourseContent.map((content) => {
4643
const videoProgressPercent =
4744
content.type === 'video' &&
4845
content.videoFullDuration &&
4946
content.duration
5047
? (content.duration / content.videoFullDuration) * 100
51-
: 0;
48+
: content.percentComplete || 0;
49+
5250
return (
5351
<ContentCard
5452
type={content.type}

src/components/NotionRenderer.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,16 @@ import { Loader } from './Loader';
2323
// import { Download } from 'lucide-react';
2424
import { useTheme } from 'next-themes';
2525
import CodeBlock from './CodeBlock';
26+
import { handleMarkAsCompleted } from '@/lib/utils';
2627

2728
// Week-4-1-647987d9b1894c54ba5c822978377910
28-
export const NotionRenderer = ({ id }: { id: string }) => {
29+
export const NotionRenderer = ({
30+
id,
31+
courseId,
32+
}: {
33+
id: string;
34+
courseId: number;
35+
}) => {
2936
const { resolvedTheme } = useTheme();
3037

3138
const [data, setData] = useState(null);
@@ -37,6 +44,10 @@ export const NotionRenderer = ({ id }: { id: string }) => {
3744

3845
useEffect(() => {
3946
main();
47+
48+
return () => {
49+
handleMarkAsCompleted(true, courseId);
50+
};
4051
}, [id]);
4152

4253
if (!data) {

0 commit comments

Comments
 (0)