Skip to content

Commit

Permalink
Merge pull request #245 from nimit9/feat/bookmarks
Browse files Browse the repository at this point in the history
Feat/bookmarks
  • Loading branch information
hkirat authored Apr 1, 2024
2 parents 7dba999 + e49737d commit e92ebe3
Show file tree
Hide file tree
Showing 25 changed files with 573 additions and 32 deletions.
35 changes: 35 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-navigation-menu": "^1.1.4",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tooltip": "^1.0.7",
"@types/bcrypt": "^5.0.2",
"@types/jsonwebtoken": "^9.0.5",
"axios": "^1.6.2",
Expand Down
22 changes: 22 additions & 0 deletions prisma/migrations/20240318200949_add_bookmark/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- CreateTable
CREATE TABLE "Bookmark" (
"id" SERIAL NOT NULL,
"userId" TEXT NOT NULL,
"contentId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"courseId" INTEGER NOT NULL,

CONSTRAINT "Bookmark_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "Bookmark_contentId_key" ON "Bookmark"("contentId");

-- AddForeignKey
ALTER TABLE "Bookmark" ADD CONSTRAINT "Bookmark_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Bookmark" ADD CONSTRAINT "Bookmark_contentId_fkey" FOREIGN KEY ("contentId") REFERENCES "Content"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Bookmark" ADD CONSTRAINT "Bookmark_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE CASCADE ON UPDATE CASCADE;
15 changes: 14 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ model Course {
slug String
content CourseContent[]
purchasedBy UserPurchases[]
bookmarks Bookmark[]
}

model UserPurchases {
Expand Down Expand Up @@ -49,6 +50,7 @@ model Content {
notionMetadataId Int?
comments Comment[]
commentsCount Int @default(0)
bookmark Bookmark?
}

model CourseContent {
Expand Down Expand Up @@ -126,6 +128,7 @@ model User {
votes Vote[]
discordConnect DiscordConnect?
disableDrm Boolean @default(false)
bookmarks Bookmark[]
password String?
appxUserId String?
appxUsername String?
Expand Down Expand Up @@ -158,6 +161,17 @@ model VideoProgress {
@@unique([contentId, userId])
}

model Bookmark {
id Int @id @default(autoincrement())
userId String
contentId Int @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
content Content @relation(fields: [contentId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
courseId Int
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
}

model Comment {
id Int @id @default(autoincrement())
content String
Expand Down Expand Up @@ -200,4 +214,3 @@ enum CommentType {
INTRO
DEFAULT
}

80 changes: 80 additions & 0 deletions src/actions/bookmark/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
'use server';
import { createSafeAction } from '@/lib/create-safe-action';
import { BookmarkCreateSchema, BookmarkDeleteSchema } from './schema';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { rateLimit } from '@/lib/utils';
import db from '@/db';
import {
InputTypeCreateBookmark,
InputTypeDeleteBookmark,
ReturnTypeCreateBookmark,
} from './types';
import { revalidatePath } from 'next/cache';

const reloadBookmarkPage = (courseId: number) => {
revalidatePath(`/courses/${courseId}/bookmarks`);
};

const createBookmarkHandler = async (
data: InputTypeCreateBookmark,
): Promise<ReturnTypeCreateBookmark> => {
const session = await getServerSession(authOptions);

if (!session || !session.user) {
return { error: 'Unauthorized or insufficient permissions' };
}

const { contentId, courseId } = data;
const userId = session.user.id;

if (!rateLimit(userId)) {
return { error: 'Rate limit exceeded. Please try again later.' };
}

try {
const addedBookmark = await db.bookmark.create({
data: { contentId, userId, courseId },
});
reloadBookmarkPage(courseId);
return { data: addedBookmark };
} catch (error: any) {
return { error: error.message || 'Failed to create comment.' };
}
};

const deleteBookmarkHandler = async (
data: InputTypeDeleteBookmark,
): Promise<any> => {
const session = await getServerSession(authOptions);

if (!session || !session.user) {
return { error: 'Unauthorized or insufficient permissions' };
}

const { id, courseId } = data;
const userId = session.user.id;

if (!rateLimit(userId)) {
return { error: 'Rate limit exceeded. Please try again later.' };
}

try {
const deletedBookmark = await db.bookmark.delete({
where: { id },
});
reloadBookmarkPage(courseId);
return { data: deletedBookmark };
} catch (error: any) {
return { error: error.message || 'Failed to create comment.' };
}
};

export const createBookmark = createSafeAction(
BookmarkCreateSchema,
createBookmarkHandler,
);
export const deleteBookmark = createSafeAction(
BookmarkDeleteSchema,
deleteBookmarkHandler,
);
10 changes: 10 additions & 0 deletions src/actions/bookmark/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { z } from 'zod';

export const BookmarkCreateSchema = z.object({
contentId: z.number(),
courseId: z.number(),
});
export const BookmarkDeleteSchema = z.object({
id: z.number(),
courseId: z.number(),
});
19 changes: 19 additions & 0 deletions src/actions/bookmark/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { z } from 'zod';
import { BookmarkCreateSchema, BookmarkDeleteSchema } from './schema';
import { ActionState } from '@/lib/create-safe-action';
import { Bookmark, Content } from '@prisma/client';

export type InputTypeCreateBookmark = z.infer<typeof BookmarkCreateSchema>;
export type ReturnTypeCreateBookmark = ActionState<
InputTypeCreateBookmark,
Bookmark
>;
export type InputTypeDeleteBookmark = z.infer<typeof BookmarkDeleteSchema>;
export type ReturnTypeDeleteBookmark = ActionState<
InputTypeDeleteBookmark,
Bookmark
>;

export type TBookmarkWithContent = Bookmark & {
content: Content & { parent?: Content | null };
};
65 changes: 52 additions & 13 deletions src/app/courses/[...courseId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import React from 'react';
import { Course } from '@/store/atoms';
import {
Content,
Folder,
Video,
getCourse,
getFullCourseContent,
} from '@/db/course';
import { Folder, Video, getCourse, getFullCourseContent } from '@/db/course';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { getPurchases } from '@/utiles/appx';
import { redirect } from 'next/navigation';
import { CourseView } from '@/components/CourseView';
import { QueryParams } from '@/actions/types';

import { Content } from '@prisma/client';
import { TBookmarkWithContent } from '@/actions/bookmark/types';
import db from '@/db';
import { rateLimit } from '@/lib/utils';
import BookmarkView from '@/components/bookmark/BookmarkView';

interface PurchaseType {
id: number;
title: string;
Expand All @@ -27,6 +25,34 @@ interface PurchaseType {
totalVideosWatched: number;
}

const getBookmarkData = async (
courseId: string,
): Promise<TBookmarkWithContent[] | { error: string }> => {
const session = await getServerSession(authOptions);
const userId = session.user.id;

if (!rateLimit(userId)) {
return { error: 'Rate limit exceeded. Please try again later.' };
}

return await db.bookmark.findMany({
where: {
userId,
courseId: parseInt(courseId, 10),
},
include: {
content: {
include: {
parent: true,
},
},
},
orderBy: {
createdAt: 'desc',
},
});
};

const checkAccess = async (courseId: string) => {
const session = await getServerSession(authOptions);

Expand Down Expand Up @@ -78,6 +104,23 @@ export default async function Course({
const fullCourseContent: Folder[] = await getFullCourseContent(
parseInt(courseId, 10),
);

if (!hasAccess) {
redirect('/api/auth/signin');
}

if (params.courseId[1] === 'bookmarks') {
const bookmarkData = await getBookmarkData(courseId);

return (
<BookmarkView
bookmarkData={bookmarkData}
courseId={course.id}
fullCourseContent={fullCourseContent}
/>
);
}

const courseContent = findContentById(
fullCourseContent,
rest.map((x) => parseInt(x, 10)),
Expand All @@ -86,10 +129,6 @@ export default async function Course({
courseContent?.length === 1 ? courseContent[0]?.type : 'folder';
const nextContent = null; //await getNextVideo(Number(rest[rest.length - 1]))

if (!hasAccess) {
redirect('/api/auth/signin');
}

return (
<>
<CourseView
Expand Down
2 changes: 1 addition & 1 deletion src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,4 @@

.vjs-duration {
display: block !important;
}
}
23 changes: 22 additions & 1 deletion src/components/Appbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useSession } from 'next-auth/react';
import { useRecoilState } from 'recoil';
import { sidebarOpen as sidebarOpenAtom } from '../store/atoms/sidebar';
import { ToggleButton } from './Sidebar';
import { usePathname } from 'next/navigation';
import { useParams, usePathname } from 'next/navigation';
import Logo from './landing/logo/logo';
import { Button } from './ui/button';
import { Sparkles } from 'lucide-react';
Expand All @@ -19,6 +19,12 @@ export const Appbar = () => {
const session = useSession();
const [sidebarOpen, setSidebarOpen] = useRecoilState(sidebarOpenAtom);
const currentPath = usePathname();
const params = useParams();
let bookmarkPageUrl = null;
if (params.courseId && params.courseId[0]) {
bookmarkPageUrl = `/courses/${params.courseId[0]}/bookmarks`;
}

return (
<>
<nav className="fixed z-50 top-0 px-4 w-full h-16 border-b shadow-sm bg-background/80 backdrop-blur-md flex items-center gap-2 print:hidden">
Expand All @@ -36,6 +42,21 @@ export const Appbar = () => {
{session?.data?.user ? (
<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>
Expand Down
Loading

0 comments on commit e92ebe3

Please sign in to comment.