Skip to content

Commit

Permalink
feat:Added Filter Button to filter content by watched, watching & unw…
Browse files Browse the repository at this point in the history
…atched videos and optimised Admin Panel (code100x#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]>
  • Loading branch information
KitsuneKode and devsargam authored Dec 6, 2024
1 parent f2596bb commit 99dd0fa
Show file tree
Hide file tree
Showing 15 changed files with 369 additions and 75 deletions.
16 changes: 5 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,11 @@ chmod +x setup.sh

```bash
docker run -d \

--name cms-db \

-e POSTGRES_USER=myuser \

-e POSTGRES_USER=myuser \
-e POSTGRES_PASSWORD=mypassword \

-e POSTGRES_DB=mydatabase \

-e POSTGRES_DB=mydatabase \
-p 5432:5432 \

postgres
```

Expand All @@ -69,7 +63,7 @@ pnpm install
3. Run database migrations:

```bash
pnpm run prisma:migrate
pnpm prisma:migrate
```

4. Generate prisma client
Expand All @@ -81,13 +75,13 @@ pnpm prisma generate
5. Seed the database:

```bash
pnpm run db:seed
pnpm db:seed
```

6. Start the development server:

```bash
pnpm run dev
pnpm dev
```

## Usage
Expand Down
41 changes: 41 additions & 0 deletions src/app/api/course/videoProgress/duration/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from 'next/server';
import db from '@/db';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { z } from 'zod';

const requestBodySchema = z.object({
contentId: z.number(),
duration: z.number(),
});

export async function POST(req: NextRequest) {
const parseResult = requestBodySchema.safeParse(await req.json());

if (!parseResult.success) {
return NextResponse.json(
{ error: parseResult.error.message },
{ status: 400 },
);
}
const { contentId, duration } = parseResult.data;
const session = await getServerSession(authOptions);
if (!session || !session?.user) {
return NextResponse.json({}, { status: 401 });
}

const updatedRecord = await db.videoMetadata.upsert({
where: {
contentId: Number(contentId),
},
create: {
contentId: Number(contentId),
duration: Number(duration),
},
update: {
duration,
},
});

return NextResponse.json(updatedRecord);
}
12 changes: 10 additions & 2 deletions src/app/courses/[courseId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { QueryParams } from '@/actions/types';
import { FilterContent } from '@/components/FilterContent';
import { Sidebar } from '@/components/Sidebar';
import { getFullCourseContent } from '@/db/course';
import { authOptions } from '@/lib/auth';
Expand Down Expand Up @@ -46,10 +47,17 @@ const Layout = async ({
}

const fullCourseContent = await getFullCourseContent(parseInt(courseId, 10));

return (
<div className="relative flex min-h-screen flex-col py-24">
<Sidebar fullCourseContent={fullCourseContent} courseId={courseId} />
<div className="flex justify-between">
<div className="2/3">
<Sidebar fullCourseContent={fullCourseContent} courseId={courseId} />
</div>
<div className="w-1/3">
<FilterContent />
</div>
</div>

{children}
</div>
);
Expand Down
40 changes: 29 additions & 11 deletions src/components/ContentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import { formatTime } from '@/lib/utils';
import VideoThumbnail from './videothumbnail';
import CardComponent from './CardComponent';
import { motion } from 'framer-motion';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from './ui/tooltip';
import React from 'react';

export const ContentCard = ({
Expand Down Expand Up @@ -40,7 +45,9 @@ export const ContentCard = ({
onClick={onClick}
tabIndex={0}
role="button"
onKeyDown={(e: React.KeyboardEvent) => (['Enter', ' '].includes(e.key) && onClick())}
onKeyDown={(e: React.KeyboardEvent) =>
['Enter', ' '].includes(e.key) && onClick()
}
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`}
>
{markAsCompleted && (
Expand All @@ -57,7 +64,9 @@ export const ContentCard = ({
<div className="relative overflow-hidden rounded-md">
<CardComponent
title={title}
contentDuration={contentDuration && formatTime(contentDuration)}
contentDuration={
contentDuration && formatTime(contentDuration)
}
type={type}
/>
{!!videoProgressPercent && (
Expand All @@ -76,10 +85,18 @@ export const ContentCard = ({
title={title}
contentId={contentId ?? 0}
imageUrl=""
// imageUrl={
// 'https://d2szwvl7yo497w.cloudfront.net/courseThumbnails/video.png'
// }
// imageUrl={
// 'https://d2szwvl7yo497w.cloudfront.net/courseThumbnails/video.png'
// }
/>
{!!videoProgressPercent && (
<div className="absolute bottom-0 h-1 w-full bg-[#707071]">
<div
className="h-full bg-[#5eff01]"
style={{ width: `${videoProgressPercent}%` }}
></div>
</div>
)}
</div>
)}
<div className="flex items-center justify-between gap-4">
Expand All @@ -98,11 +115,12 @@ export const ContentCard = ({
</div>
</motion.div>
</TooltipTrigger>
{
Array.isArray(weeklyContentTitles) && weeklyContentTitles?.length > 0 && <TooltipContent sideOffset={16}>
{weeklyContentTitles?.map((title) => <p>{title}</p>)}
</TooltipContent>
}
{Array.isArray(weeklyContentTitles) &&
weeklyContentTitles?.length > 0 && (
<TooltipContent sideOffset={16}>
{weeklyContentTitles?.map((title) => <p>{title}</p>)}
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
);
Expand Down
8 changes: 4 additions & 4 deletions src/components/CourseView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,11 @@ export const CourseView = ({
</div>

{!courseContent?.folder && courseContent?.value.type === 'notion' ? (
<NotionRenderer id={courseContent?.value?.id?.toString()} />
<NotionRenderer
id={courseContent?.value?.id?.toString()}
courseId={courseContent.value.id}
/>
) : null}

{!courseContent?.folder && (contentType === 'video' || contentType === 'appx') ? (
<ContentRenderer
nextContent={nextContent}
Expand All @@ -70,7 +72,6 @@ export const CourseView = ({
}}
/>
) : null}

{!courseContent?.folder &&
(contentType === 'video' || contentType === 'notion') && (
<Comments
Expand All @@ -84,7 +85,6 @@ export const CourseView = ({
searchParams={searchParams}
/>
)}

{courseContent?.folder ? (
<FolderView
rest={rest}
Expand Down
82 changes: 82 additions & 0 deletions src/components/FilterContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use client';
import React, { useState, forwardRef } from 'react';
import { Check, ChevronsUpDown } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { useRecoilState } from 'recoil';
import { selectFilter } from '@/store/atoms/filterContent';
import {
Command,
CommandGroup,
CommandItem,
CommandList,
} from '@/components/ui/command';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';

const allFilters = [
{ value: 'all', label: 'ALL' },
{ value: 'unwatched', label: 'Unwatched' },
{ value: 'watched', label: 'Watched' },
{ value: 'watching', label: 'Watching' },
];

type FilterContentProps = {
// Add any other props here if needed
className?: string;
};

export const FilterContent = forwardRef<HTMLDivElement, FilterContentProps>(
(props, ref) => {
const [open, setOpen] = useState(false);
const [value, setValue] = useRecoilState(selectFilter);

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={`w-fit gap-2 ${props.className || ''}`}
>
{value
? allFilters.find((filters) => filters.value === value)?.label
: 'Filter'}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="z-[99999] w-fit p-0" ref={ref}>
<Command>
<CommandList>
<CommandGroup>
{allFilters.map((filters) => (
<CommandItem
key={filters.value}
value={filters.value}
className={`px-4 ${props.className || ''}`}
onSelect={(currentValue) => {
setValue(currentValue === value ? '' : currentValue);
setOpen(false);
}}
>
<Check
className={cn(
'mr-2 h-4 w-4',
value === filters.value ? 'opacity-100' : 'opacity-0',
)}
/>
{filters.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
},
);
30 changes: 14 additions & 16 deletions src/components/FolderView.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
'use client';
import { useRouter } from 'next/navigation';
import { ContentCard } from './ContentCard';
import { Bookmark } from '@prisma/client';
import { CourseContentType } from '@/lib/utils';
import { courseContent, getFilteredContent } from '@/lib/utils';
import { useRecoilValue } from 'recoil';
import { selectFilter } from '@/store/atoms/filterContent';

export const FolderView = ({
courseContent,
Expand All @@ -11,18 +12,7 @@ export const FolderView = ({
}: {
courseId: number;
rest: string[];
courseContent: {
type: CourseContentType;
title: string;
image: string;
id: number;
markAsCompleted: boolean;
percentComplete: number | null;
videoFullDuration?: number;
duration?: number;
bookmark: Bookmark | null;
weeklyContentTitles?: string[];
}[];
courseContent: courseContent[];
}) => {
const router = useRouter();

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

const currentfilter = useRecoilValue(selectFilter);

const filteredCourseContent = getFilteredContent(
courseContent,
currentfilter,
);

return (
<div>
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{courseContent.map((content) => {
{filteredCourseContent.map((content) => {
const videoProgressPercent =
content.type === 'video' &&
content.videoFullDuration &&
content.duration
? (content.duration / content.videoFullDuration) * 100
: 0;
: content.percentComplete || 0;

return (
<ContentCard
type={content.type}
Expand Down
13 changes: 12 additions & 1 deletion src/components/NotionRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,16 @@ import { Loader } from './Loader';
// import { Download } from 'lucide-react';
import { useTheme } from 'next-themes';
import CodeBlock from './CodeBlock';
import { handleMarkAsCompleted } from '@/lib/utils';

// Week-4-1-647987d9b1894c54ba5c822978377910
export const NotionRenderer = ({ id }: { id: string }) => {
export const NotionRenderer = ({
id,
courseId,
}: {
id: string;
courseId: number;
}) => {
const { resolvedTheme } = useTheme();

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

useEffect(() => {
main();

return () => {
handleMarkAsCompleted(true, courseId);
};
}, [id]);

if (!data) {
Expand Down
Loading

0 comments on commit 99dd0fa

Please sign in to comment.