From 7a5d31bdb18137908f467a40b600a738ce49fdcc Mon Sep 17 00:00:00 2001 From: ikramBagban <107988060+IkramBagban@users.noreply.github.com> Date: Mon, 20 Jan 2025 21:03:17 +0530 Subject: [PATCH 1/8] feat(schema): add pinned courses schema to database --- .../20250120153105_add_pinned_courses/migration.sql | 13 +++++++++++++ prisma/schema.prisma | 12 ++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 prisma/migrations/20250120153105_add_pinned_courses/migration.sql diff --git a/prisma/migrations/20250120153105_add_pinned_courses/migration.sql b/prisma/migrations/20250120153105_add_pinned_courses/migration.sql new file mode 100644 index 000000000..d2472d2c0 --- /dev/null +++ b/prisma/migrations/20250120153105_add_pinned_courses/migration.sql @@ -0,0 +1,13 @@ +-- CreateTable +CREATE TABLE "PinnedCourses" ( + "userId" TEXT NOT NULL, + "courseId" INTEGER NOT NULL, + + CONSTRAINT "PinnedCourses_pkey" PRIMARY KEY ("userId","courseId") +); + +-- AddForeignKey +ALTER TABLE "PinnedCourses" ADD CONSTRAINT "PinnedCourses_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PinnedCourses" ADD CONSTRAINT "PinnedCourses_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ca8e4431c..9bd440310 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -22,6 +22,7 @@ model Course { purchasedBy UserPurchases[] certificate Certificate[] certIssued Boolean @default(false) + PinnedCourses PinnedCourses[] } model UserPurchases { @@ -164,6 +165,17 @@ model User { solanaAddresses SolanaAddress[] @relation("UserSolanaAddresses") githubUser GitHubLink? @relation("UserGithub") bounties BountySubmission[] + PinnedCourses PinnedCourses[] +} + +model PinnedCourses { + userId String + courseId Int + + user User @relation(fields: [userId], references: [id]) + course Course @relation(fields: [courseId], references: [id]) + + @@id([userId, courseId]) } model GitHubLink { From 041e74f5e493a16c2a7394c79ba2f0ec61a4474e Mon Sep 17 00:00:00 2001 From: ikramBagban <107988060+IkramBagban@users.noreply.github.com> Date: Mon, 20 Jan 2025 21:29:36 +0530 Subject: [PATCH 2/8] feat: add pin, unpin icons with toggle functionality in CourseCard component --- src/components/CourseCard.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/components/CourseCard.tsx b/src/components/CourseCard.tsx index ecf8cc6ae..c6be08873 100644 --- a/src/components/CourseCard.tsx +++ b/src/components/CourseCard.tsx @@ -6,6 +6,9 @@ import { Button } from './ui/button'; import { Card, CardContent } from "@/components/ui/card"; import { motion } from "framer-motion"; import { MessageCircle, PlayCircle } from "lucide-react"; +import { TbPinned } from "react-icons/tb"; +import { TbPinnedFilled } from "react-icons/tb"; +import { useState } from 'react'; export const CourseCard = ({ course, @@ -15,12 +18,26 @@ export const CourseCard = ({ onClick: () => void; }) => { const router = useRouter(); + const [pinnedCourse, setPinnedCourse] = useState(false); + const imageUrl = course.imageUrl ? course.imageUrl : 'banner_placeholder.png'; const percentage = course.totalVideos !== undefined ? Math.ceil(((course.totalVideosWatched ?? 0) / course?.totalVideos) * 100) : 0; return (
+
{ + e.stopPropagation(); + setPinnedCourse(p => !p); + }} + > + { + pinnedCourse ? : + } +
+ {course.title} Date: Fri, 31 Jan 2025 21:18:57 +0530 Subject: [PATCH 3/8] feat(pinCourse): implement toggle action for pinning and unpinning courses --- src/actions/pinCourse/index.ts | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/actions/pinCourse/index.ts diff --git a/src/actions/pinCourse/index.ts b/src/actions/pinCourse/index.ts new file mode 100644 index 000000000..0e2e427fc --- /dev/null +++ b/src/actions/pinCourse/index.ts @@ -0,0 +1,37 @@ +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/lib/auth'; +import prisma from '@/db'; + +export async function togglePinCourse(courseId: number) { + const session = await getServerSession(authOptions); + + if (!session?.user?.id) { + return { error: true, message: 'User not authenticated', isPinned: false }; + } + + try { + const userId = session.user.id; + + const existingPin = await prisma.pinnedCourses.findUnique({ + where: { + userId_courseId: { userId, courseId }, + }, + }); + + if (existingPin) { + await prisma.pinnedCourses.delete({ + where: { + userId_courseId: { userId, courseId }, + }, + }); + return { error: false, message: 'Course unpinned successfully', isPinned: false }; + } + await prisma.pinnedCourses.create({ + data: { userId, courseId }, + }); + return { error: false, message: 'Course pinned successfully', isPinned: true }; + } catch (error) { + console.error('Error toggling pin:', error); + return { error: true, message: 'Failed to toggle pin course', isPinned: false }; + } +} From b17e63f92ebbad90f8088cd8652b8aabe3bdbbbd Mon Sep 17 00:00:00 2001 From: ikramBagban <107988060+IkramBagban@users.noreply.github.com> Date: Sat, 1 Feb 2025 00:26:24 +0530 Subject: [PATCH 4/8] fix: remove 'PinnedCourses' table with it's migration --- .../20250124185937_add_is_pinned/migration.sql | 3 +++ .../migration.sql | 8 ++++++++ .../20250124190616_add_is_pinned/migration.sql | 2 ++ .../migration.sql | 18 ++++++++++++++++++ prisma/schema.prisma | 12 ------------ 5 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 prisma/migrations/20250124185937_add_is_pinned/migration.sql create mode 100644 prisma/migrations/20250124190548_delete_is_pinned/migration.sql create mode 100644 prisma/migrations/20250124190616_add_is_pinned/migration.sql create mode 100644 prisma/migrations/20250131185345_remove_pinned_courses_table/migration.sql diff --git a/prisma/migrations/20250124185937_add_is_pinned/migration.sql b/prisma/migrations/20250124185937_add_is_pinned/migration.sql new file mode 100644 index 000000000..8e48533fe --- /dev/null +++ b/prisma/migrations/20250124185937_add_is_pinned/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "UserPurchases" ADD COLUMN "isPinned" BOOLEAN NOT NULL DEFAULT false; + \ No newline at end of file diff --git a/prisma/migrations/20250124190548_delete_is_pinned/migration.sql b/prisma/migrations/20250124190548_delete_is_pinned/migration.sql new file mode 100644 index 000000000..29e7200c9 --- /dev/null +++ b/prisma/migrations/20250124190548_delete_is_pinned/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `isPinned` on the `UserPurchases` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "UserPurchases" DROP COLUMN "isPinned"; diff --git a/prisma/migrations/20250124190616_add_is_pinned/migration.sql b/prisma/migrations/20250124190616_add_is_pinned/migration.sql new file mode 100644 index 000000000..5fecc493a --- /dev/null +++ b/prisma/migrations/20250124190616_add_is_pinned/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "UserPurchases" ADD COLUMN "isPinned" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/migrations/20250131185345_remove_pinned_courses_table/migration.sql b/prisma/migrations/20250131185345_remove_pinned_courses_table/migration.sql new file mode 100644 index 000000000..6e786f56f --- /dev/null +++ b/prisma/migrations/20250131185345_remove_pinned_courses_table/migration.sql @@ -0,0 +1,18 @@ +/* + Warnings: + + - You are about to drop the column `isPinned` on the `UserPurchases` table. All the data in the column will be lost. + - You are about to drop the `PinnedCourses` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "PinnedCourses" DROP CONSTRAINT "PinnedCourses_courseId_fkey"; + +-- DropForeignKey +ALTER TABLE "PinnedCourses" DROP CONSTRAINT "PinnedCourses_userId_fkey"; + +-- AlterTable +ALTER TABLE "UserPurchases" DROP COLUMN "isPinned"; + +-- DropTable +DROP TABLE "PinnedCourses"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9bd440310..ca8e4431c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -22,7 +22,6 @@ model Course { purchasedBy UserPurchases[] certificate Certificate[] certIssued Boolean @default(false) - PinnedCourses PinnedCourses[] } model UserPurchases { @@ -165,17 +164,6 @@ model User { solanaAddresses SolanaAddress[] @relation("UserSolanaAddresses") githubUser GitHubLink? @relation("UserGithub") bounties BountySubmission[] - PinnedCourses PinnedCourses[] -} - -model PinnedCourses { - userId String - courseId Int - - user User @relation(fields: [userId], references: [id]) - course Course @relation(fields: [courseId], references: [id]) - - @@id([userId, courseId]) } model GitHubLink { From 68ec14fc8e435afa8a521207cb3f2adbb7d1bbd1 Mon Sep 17 00:00:00 2001 From: ikramBagban <107988060+IkramBagban@users.noreply.github.com> Date: Sat, 1 Feb 2025 00:27:58 +0530 Subject: [PATCH 5/8] feat: implement pin course functionality --- src/components/CourseCard.tsx | 10 ++++++---- src/components/Courses.tsx | 27 ++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/components/CourseCard.tsx b/src/components/CourseCard.tsx index c6be08873..89db8b389 100644 --- a/src/components/CourseCard.tsx +++ b/src/components/CourseCard.tsx @@ -8,17 +8,19 @@ import { motion } from "framer-motion"; import { MessageCircle, PlayCircle } from "lucide-react"; import { TbPinned } from "react-icons/tb"; import { TbPinnedFilled } from "react-icons/tb"; -import { useState } from 'react'; export const CourseCard = ({ course, onClick, + isPinned, + onPinToggle, }: { course: Course; onClick: () => void; + isPinned: boolean; + onPinToggle: () => void; }) => { const router = useRouter(); - const [pinnedCourse, setPinnedCourse] = useState(false); const imageUrl = course.imageUrl ? course.imageUrl : 'banner_placeholder.png'; const percentage = course.totalVideos !== undefined ? Math.ceil(((course.totalVideosWatched ?? 0) / course?.totalVideos) * 100) : 0; @@ -30,11 +32,11 @@ export const CourseCard = ({ className="absolute right-5 top-5 z-50" onClick={(e) => { e.stopPropagation(); - setPinnedCourse(p => !p); + onPinToggle(); }} > { - pinnedCourse ? : + isPinned ? : }
diff --git a/src/components/Courses.tsx b/src/components/Courses.tsx index 416fae1cd..c4b39c5c2 100644 --- a/src/components/Courses.tsx +++ b/src/components/Courses.tsx @@ -8,10 +8,33 @@ import { refreshDb } from '@/actions/refresh-db'; import { useSession } from 'next-auth/react'; import { toast } from 'sonner'; import Link from 'next/link'; +import { useState } from 'react'; export const Courses = ({ courses }: { courses: Course[] }) => { + const [pinnedCourseIds, setPinnedCourseIds] = useState(() => { + const stored = localStorage.getItem('pinnedCourses'); + return stored ? JSON.parse(stored) : []; + }); const session = useSession(); + const togglePin = (courseId: number) => { + setPinnedCourseIds(prev => { + const newPinned = prev.includes(courseId) + ? prev.filter(id => id !== courseId) + : [...prev, courseId]; + localStorage.setItem('pinnedCourses', JSON.stringify(newPinned)); + return newPinned; + }); + }; + + const sortedCourses = [...courses].sort((a: Course, b: Course) : any => { + if (!pinnedCourseIds) return; + const aPinned = pinnedCourseIds.includes(a.id); + const bPinned = pinnedCourseIds.includes(b.id); + if (aPinned === bPinned) return 0; + return -1; + }); + const handleClick = async () => { // @ts-ignore const res = await refreshDb({ userId: session.data.user.id }); @@ -24,10 +47,12 @@ export const Courses = ({ courses }: { courses: Course[] }) => { const router = useRouter(); return (
- {courses?.map((course) => ( + {sortedCourses?.map((course) => ( togglePin(course.id)} onClick={() => { if ( course.title.includes('Machine Learning') || From 2d36ff469b577098241e1abb041192306eb7c261 Mon Sep 17 00:00:00 2001 From: ikramBagban <107988060+IkramBagban@users.noreply.github.com> Date: Sat, 1 Feb 2025 00:56:00 +0530 Subject: [PATCH 6/8] fix: resolve 'ReferenceError: localStorage is not defined' & hydration error --- src/components/Courses.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/Courses.tsx b/src/components/Courses.tsx index c4b39c5c2..85dad1d85 100644 --- a/src/components/Courses.tsx +++ b/src/components/Courses.tsx @@ -8,16 +8,21 @@ import { refreshDb } from '@/actions/refresh-db'; import { useSession } from 'next-auth/react'; import { toast } from 'sonner'; import Link from 'next/link'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; export const Courses = ({ courses }: { courses: Course[] }) => { - const [pinnedCourseIds, setPinnedCourseIds] = useState(() => { - const stored = localStorage.getItem('pinnedCourses'); - return stored ? JSON.parse(stored) : []; - }); + const [pinnedCourseIds, setPinnedCourseIds] = useState([]); const session = useSession(); + useEffect(() => { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem('pinnedCourses'); + setPinnedCourseIds(stored ? JSON.parse(stored) : []); + } + }, []); + const togglePin = (courseId: number) => { + if (typeof window === 'undefined') return; setPinnedCourseIds(prev => { const newPinned = prev.includes(courseId) ? prev.filter(id => id !== courseId) From c2a9916ff08d8f50f74a1572ee9ddfd3186a7c8a Mon Sep 17 00:00:00 2001 From: ikramBagban <107988060+IkramBagban@users.noreply.github.com> Date: Sat, 1 Feb 2025 01:01:55 +0530 Subject: [PATCH 7/8] fix(pinCourse): remove togglePinCourse action and its implementation --- src/actions/pinCourse/index.ts | 37 ---------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 src/actions/pinCourse/index.ts diff --git a/src/actions/pinCourse/index.ts b/src/actions/pinCourse/index.ts deleted file mode 100644 index 0e2e427fc..000000000 --- a/src/actions/pinCourse/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/lib/auth'; -import prisma from '@/db'; - -export async function togglePinCourse(courseId: number) { - const session = await getServerSession(authOptions); - - if (!session?.user?.id) { - return { error: true, message: 'User not authenticated', isPinned: false }; - } - - try { - const userId = session.user.id; - - const existingPin = await prisma.pinnedCourses.findUnique({ - where: { - userId_courseId: { userId, courseId }, - }, - }); - - if (existingPin) { - await prisma.pinnedCourses.delete({ - where: { - userId_courseId: { userId, courseId }, - }, - }); - return { error: false, message: 'Course unpinned successfully', isPinned: false }; - } - await prisma.pinnedCourses.create({ - data: { userId, courseId }, - }); - return { error: false, message: 'Course pinned successfully', isPinned: true }; - } catch (error) { - console.error('Error toggling pin:', error); - return { error: true, message: 'Failed to toggle pin course', isPinned: false }; - } -} From 5ec9a290ba52bb97eefff3e8d5a35f404d29c307 Mon Sep 17 00:00:00 2001 From: ikramBagban <107988060+IkramBagban@users.noreply.github.com> Date: Sat, 1 Feb 2025 01:14:46 +0530 Subject: [PATCH 8/8] fix(CourseCard): make isPinned and onPinToggle optional props --- src/components/CourseCard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CourseCard.tsx b/src/components/CourseCard.tsx index 89db8b389..940024517 100644 --- a/src/components/CourseCard.tsx +++ b/src/components/CourseCard.tsx @@ -17,8 +17,8 @@ export const CourseCard = ({ }: { course: Course; onClick: () => void; - isPinned: boolean; - onPinToggle: () => void; + isPinned?: boolean; + onPinToggle?: () => void; }) => { const router = useRouter();