diff --git a/package.json b/package.json index 8e29be4a5..303604a68 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "notion-client": "^6.16.0", "pdf-lib": "^1.17.1", "react": "^18", + "react-big-calendar": "^1.13.2", "react-dom": "^18", "react-hook-form": "^7.50.1", "react-icons": "^5.1.0", @@ -105,6 +106,7 @@ "@testing-library/react": "^15.0.7", "@types/node": "^20", "@types/react": "^18", + "@types/react-big-calendar": "^1.8.9", "@types/react-dom": "^18", "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.20.0", diff --git a/src/actions/calendar/index.ts b/src/actions/calendar/index.ts new file mode 100644 index 000000000..43325d44a --- /dev/null +++ b/src/actions/calendar/index.ts @@ -0,0 +1,52 @@ +'use server'; + +import db from '@/db'; +import { ContentType } from './types'; +import { cache } from '@/db/Cache'; +import { getEventsSchema } from './schema'; + +export const getEvents = async (courseId: number): Promise => { + try { + const { success } = getEventsSchema.safeParse({ courseId }); + + if (!success) { + console.log('Parsing failed'); + return []; + } + const value = await cache.get('getEvents', [courseId.toString()]); + if (value) { + return value; + } + const folders = await db.courseContent.findMany({ + where: { + courseId, + }, + include: { + content: { + select: { + children: true, + }, + }, + }, + }); + + const content: ContentType[] = []; + folders.forEach((folder) => { + folder.content.children.forEach((el) => { + if (el.type === 'video') { + content.push({ + id: el.id, + title: el.title, + start: el.createdAt, + end: el.createdAt, + }); + } + }); + }); + cache.set('getEvents', [courseId.toString()], content); + return content; + } catch (err) { + console.error(err); + return []; + } +}; diff --git a/src/actions/calendar/schema.ts b/src/actions/calendar/schema.ts new file mode 100644 index 000000000..9da54e34b --- /dev/null +++ b/src/actions/calendar/schema.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; + +export const getEventsSchema = z.object({ + courseId: z.number(), +}); diff --git a/src/actions/calendar/types.ts b/src/actions/calendar/types.ts new file mode 100644 index 000000000..e8122aacb --- /dev/null +++ b/src/actions/calendar/types.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; +import { getEventsSchema } from './schema'; + +export type getEventsSchemaType = z.infer; + +export type ContentType = { + id: number; + title: string; + start: Date; + end: Date; +}; diff --git a/src/app/error.tsx b/src/app/error.tsx index db6d36727..79bb12187 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -1,13 +1,11 @@ 'use client'; -import { Button } from '@/components/ui/button'; import { InfoIcon } from 'lucide-react'; import Link from 'next/link'; import { useEffect } from 'react'; export default function ErrorPage({ error, - reset, }: { error: Error & { digest?: string }; reset: () => void; diff --git a/src/components/Appbar.tsx b/src/components/Appbar.tsx index f6470b76d..154ce254e 100644 --- a/src/components/Appbar.tsx +++ b/src/components/Appbar.tsx @@ -15,11 +15,26 @@ import SearchBar from './search/SearchBar'; import MobileScreenSearch from './search/MobileScreenSearch'; import ProfileDropdown from './profile-menu/ProfileDropdown'; import { ThemeToggler } from './ThemeToggler'; +import { CalendarDialog } from './Calendar'; +import { useEffect, useState } from 'react'; +import { getEvents } from '@/actions/calendar'; +import { ContentType } from '@/actions/calendar/types'; export const Appbar = () => { const { data: session, status: sessionStatus } = useSession(); const [sidebarOpen, setSidebarOpen] = useRecoilState(sidebarOpenAtom); const currentPath = usePathname(); + const courseId = currentPath.split('/')[2]; + const [events, setEvents] = useState(); + + useEffect(() => { + async function fetchEvents() { + const fetchedEvents = (await getEvents(Number(courseId))) ?? []; + setEvents(fetchedEvents); + } + + fetchEvents(); + }, [courseId]); const isLoading = sessionStatus === 'loading'; @@ -43,9 +58,13 @@ export const Appbar = () => {
-
+
{/* Search Bar for smaller devices */} +
diff --git a/src/components/Calendar.tsx b/src/components/Calendar.tsx new file mode 100644 index 000000000..69554704e --- /dev/null +++ b/src/components/Calendar.tsx @@ -0,0 +1,83 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Calendar as CalendarIcon } from 'lucide-react'; +import { Calendar, dayjsLocalizer, View, Views } from 'react-big-calendar'; +import dayjs from 'dayjs'; +import 'react-big-calendar/lib/css/react-big-calendar.css'; +import { useCallback, useState } from 'react'; + +const localizer = dayjsLocalizer(dayjs); + +interface CalendarProps { + inCourse: boolean; + events: { id: number; title: String; start: Date; end: Date }[]; +} + +const MyCalendar = ({ + events, +}: { + events: { + id: number; + title: String; + start: Date; + end: Date; + }[]; +}) => { + const [view, setView] = useState(Views.MONTH); + + const [date, setDate] = useState(new Date()); + + const onNavigate = useCallback( + (newDate: Date) => { + return setDate(newDate); + }, + [setDate], + ); + + const handleOnChangeView = (selectedView: View) => { + setView(selectedView); + }; + + return ( +
+ +
+ ); +}; + +export const CalendarDialog = ({ inCourse, events }: CalendarProps) => { + if (!inCourse) return null; + + return ( + + + + + + + Track your progress + + + + + + + ); +}; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 000000000..2ca04ecbf --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +'use client'; + +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = 'DialogHeader'; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/tsconfig.json b/tsconfig.json index 1f308869a..cec286f06 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,14 +16,14 @@ "incremental": true, "plugins": [ { - "name": "next" - } + "name": "next", + }, ], "paths": { "@/*": ["./src/*"], - "@public/*": ["./public/*"] - } + "@public/*": ["./public/*"], + }, }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "exclude": ["node_modules"], }