Skip to content

Commit

Permalink
feat:Added Filter Button to filter out watched, watching & unwatched …
Browse files Browse the repository at this point in the history
…content
  • Loading branch information
KitsuneKode committed Oct 16, 2024
1 parent 6a49a64 commit 393884e
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 53 deletions.
10 changes: 2 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,11 @@ cd cms

```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 Down
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
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>
);
},
);
27 changes: 12 additions & 15 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,10 +29,17 @@ 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 &&
Expand Down
86 changes: 60 additions & 26 deletions src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ import {
} from '@/components/ui/accordion';
import { Play, File, X, Menu } from 'lucide-react';
import { FullCourseContent } from '@/db/course';
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { sidebarOpen as sidebarOpenAtom } from '@/store/atoms/sidebar';
import { useEffect, useState, useCallback, useMemo } from 'react';
import { handleMarkAsCompleted } from '@/lib/utils';
import BookmarkButton from './bookmark/BookmarkButton';
import Link from 'next/link';
import { Button } from './ui/button';
import { AnimatePresence, motion } from 'framer-motion';

import { FilterContent } from './FilterContent';
import { selectFilter } from '@/store/atoms/filterContent';
const sidebarVariants = {
open: {
width: '100%',
Expand Down Expand Up @@ -47,7 +48,9 @@ export function Sidebar({
>([]);
const sidebarRef = useRef<HTMLDivElement | null>(null);
const buttonRef = useRef<HTMLButtonElement | null>(null);
const filterRef = useRef<HTMLDivElement | null>(null);
const closeSidebar = () => setSidebarOpen(false);
const currentfilter = useRecoilValue(selectFilter);

const findPathToContent = useCallback(
(
Expand Down Expand Up @@ -77,7 +80,8 @@ export function Sidebar({
if (
sidebarRef.current &&
!sidebarRef.current.contains(event.target as Node) &&
!buttonRef.current?.contains(event.target as Node)
!buttonRef.current?.contains(event.target as Node) &&
!filterRef.current?.contains(event.target as Node)
) {
closeSidebar();
}
Expand Down Expand Up @@ -133,7 +137,6 @@ export function Sidebar({
(contents: FullCourseContent[]) => {
return contents.map((content) => {
const isActiveContent = currentActiveContentIds?.includes(content.id);

if (content.children && content.children.length > 0) {
return (
<AccordionItem
Expand All @@ -152,32 +155,35 @@ export function Sidebar({
}

return (
<Link
key={content.id}
href={navigateToContent(content.id) || '#'}
className={`flex w-full cursor-pointer items-center rounded-md p-4 tracking-tight hover:bg-primary/10 ${isActiveContent ? 'bg-primary/10' : ''}`}
>
<div className="flex w-full items-center justify-between gap-2">
<div className="flex items-center gap-2">
<div className="flex gap-2">
<Check content={content} />
{content.type === 'video' && <Play className="size-4" />}
{content.type === 'notion' && <File className="size-4" />}
(content.type === 'notion' ||
filterContent(currentfilter, content)) && (
<Link
key={content.id}
href={navigateToContent(content.id) || '#'}
className={`flex w-full cursor-pointer items-center rounded-md p-4 tracking-tight hover:bg-primary/10 ${isActiveContent ? 'bg-primary/10' : ''}`}
>
<div className="flex w-full items-center justify-between gap-2">
<div className="flex items-center gap-2">
<div className="flex gap-2">
<Check content={content} />
{content.type === 'video' && <Play className="size-4" />}
{content.type === 'notion' && <File className="size-4" />}
</div>
<div className="break-words text-base">{content.title}</div>
</div>
<div className="break-words text-base">{content.title}</div>
{content.type === 'video' && (
<BookmarkButton
bookmark={content.bookmark ?? null}
contentId={content.id}
/>
)}
</div>
{content.type === 'video' && (
<BookmarkButton
bookmark={content.bookmark ?? null}
contentId={content.id}
/>
)}
</div>
</Link>
</Link>
)
);
});
},
[currentActiveContentIds, navigateToContent],
[currentActiveContentIds, navigateToContent, currentfilter],
);

const memoizedContent = useMemo(
Expand Down Expand Up @@ -206,9 +212,15 @@ export function Sidebar({
variants={sidebarVariants}
className="fixed right-0 top-0 z-[99999] flex h-screen w-full flex-col gap-4 overflow-y-auto rounded-r-lg border-l border-primary/10 bg-neutral-50 dark:bg-neutral-900 md:max-w-[30vw]"
>
<div className="sticky top-0 z-10 flex items-center justify-between border-b border-primary/10 bg-neutral-50 p-5 dark:bg-neutral-900"> <h4 className="text-xl font-bold tracking-tighter text-primary lg:text-2xl">
<div className="sticky top-0 z-10 flex items-center justify-between border-b border-primary/10 bg-neutral-50 p-5 dark:bg-neutral-900">
{' '}
<h4 className="text-xl font-bold tracking-tighter text-primary lg:text-2xl">
Course Content
</h4>
<FilterContent
className="bg-gray-400 text-black"
ref={filterRef}
/>
<Button
variant="ghost"
size="icon"
Expand Down Expand Up @@ -252,3 +264,25 @@ function Check({ content }: { content: any }) {
/>
);
}

function filterContent(filter: string, content: FullCourseContent) {
if (filter === 'all' || filter === '') {
return true;
}
if (filter === 'watched') {
return content.videoProgress?.markAsCompleted;
}
if (filter === 'watching') {
return (
content.videoProgress?.markAsCompleted === false &&
content.videoProgress?.duration !== null &&
content.videoProgress?.duration !== 0
);
}
if (filter === 'unwatched') {
return (
content.videoProgress?.markAsCompleted === false &&
content.videoProgress?.duration === 0
);
}
}
47 changes: 45 additions & 2 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CommentType, Prisma } from '@prisma/client';
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import Player from 'video.js/dist/types/player';

import { Bookmark } from '@prisma/client';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Expand Down Expand Up @@ -367,4 +367,47 @@ export const getDisabledFeature = (feature: string): boolean => {
.includes(feature);
};

export type CourseContentType = 'folder' | 'video' | 'notion'
export type CourseContentType = 'folder' | 'video' | 'notion';

export type courseContent = {
type: CourseContentType;
title: string;
image: string;
id: number;
markAsCompleted: boolean;
percentComplete: number | null;
videoFullDuration?: number;
duration?: number;
bookmark: Bookmark | null;
weeklyContentTitles?: string[];
};

export function getFilteredContent(
courseContent: courseContent[],
filter: string,
): courseContent[] {
if (filter === 'all' || filter === '' || courseContent[0]?.type === 'folder')
//Hoping no folder has recursive folders
return courseContent;

if (filter === 'watched')
return courseContent?.filter(
(content) => content?.markAsCompleted === true,
);

if (filter === 'watching')
return courseContent?.filter(
(content) =>
content?.markAsCompleted === false &&
content?.duration !== null &&
content?.duration !== 0,
);

if (filter === 'unwatched')
return courseContent?.filter(
(content) =>
content?.markAsCompleted === false && content?.duration === 0,
);

return [];
}
6 changes: 6 additions & 0 deletions src/store/atoms/filterContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { atom } from 'recoil';

export const selectFilter = atom({
key: 'selectFilter',
default: '',
});

0 comments on commit 393884e

Please sign in to comment.