Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/bookmarks #245

Merged
merged 17 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading