diff --git a/package-lock.json b/package-lock.json
index b70b3a03..c33054a7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,11 +26,13 @@
"react-dom": "18.2.0",
"react-hot-toast": "^2.4.0",
"react-icons": "^4.7.1",
+ "react-stars": "^2.2.5",
"react-table": "^7.8.0",
"typescript": "4.9.4"
},
"devDependencies": {
"@types/js-cookie": "^3.0.2",
+ "@types/react-stars": "^2.2.1",
"@types/react-table": "^7.7.12",
"autoprefixer": "^10.4.13",
"postcss": "^8.4.20",
@@ -1321,6 +1323,15 @@
"@types/react": "*"
}
},
+ "node_modules/@types/react-stars": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@types/react-stars/-/react-stars-2.2.1.tgz",
+ "integrity": "sha512-x0Z7+OsImFfBbHpLm/o3Z/q9qbapQbaSbKkozSIJXs6OAkFW5keA4Qw4MOjocdmhPPUYI6DDVSB0NOm0QmY+Xg==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/react-table": {
"version": "7.7.12",
"resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-7.7.12.tgz",
@@ -5221,6 +5232,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "node_modules/react-stars": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/react-stars/-/react-stars-2.2.5.tgz",
+ "integrity": "sha512-O//KS6Z9RZnUOU/r+3+wxCzNalVn/wEWIcbAQFRV3dw8WbV+ys/kgcpBPijqTiNpHGrcwZdtb/7wF/UVem20CQ=="
+ },
"node_modules/react-table": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz",
@@ -7140,6 +7156,15 @@
"@types/react": "*"
}
},
+ "@types/react-stars": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@types/react-stars/-/react-stars-2.2.1.tgz",
+ "integrity": "sha512-x0Z7+OsImFfBbHpLm/o3Z/q9qbapQbaSbKkozSIJXs6OAkFW5keA4Qw4MOjocdmhPPUYI6DDVSB0NOm0QmY+Xg==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
"@types/react-table": {
"version": "7.7.12",
"resolved": "https://registry.npmjs.org/@types/react-table/-/react-table-7.7.12.tgz",
@@ -9938,6 +9963,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
+ "react-stars": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/react-stars/-/react-stars-2.2.5.tgz",
+ "integrity": "sha512-O//KS6Z9RZnUOU/r+3+wxCzNalVn/wEWIcbAQFRV3dw8WbV+ys/kgcpBPijqTiNpHGrcwZdtb/7wF/UVem20CQ=="
+ },
"react-table": {
"version": "7.8.0",
"resolved": "https://registry.npmjs.org/react-table/-/react-table-7.8.0.tgz",
diff --git a/package.json b/package.json
index bc4c8446..63d96b7e 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,8 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
- "lint": "next lint"
+ "lint": "next lint",
+ "postinstall": "npx prisma generate"
},
"dependencies": {
"@lottiefiles/react-lottie-player": "^3.5.0",
@@ -27,11 +28,13 @@
"react-dom": "18.2.0",
"react-hot-toast": "^2.4.0",
"react-icons": "^4.7.1",
+ "react-stars": "^2.2.5",
"react-table": "^7.8.0",
"typescript": "4.9.4"
},
"devDependencies": {
"@types/js-cookie": "^3.0.2",
+ "@types/react-stars": "^2.2.1",
"@types/react-table": "^7.7.12",
"autoprefixer": "^10.4.13",
"postcss": "^8.4.20",
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 2a338036..60db9a0a 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -8,17 +8,18 @@ datasource db {
}
model user {
- user_id String @id @default(uuid())
- email String @unique
- name String
- notes note[]
- courses course[]
- subjects subject[]
- pyqs pyq[]
- instructors instructor[]
- note_upvotes note_upvote[]
- course_upvotes course_upvote[]
- pyq_upvotes pyq_upvote[]
+ user_id String @id @default(uuid())
+ email String @unique
+ name String
+ notes note[]
+ courses course[]
+ subjects subject[]
+ pyqs pyq[]
+ instructors instructor[]
+ note_upvotes note_upvote[]
+ course_reviews course_review[]
+ pyq_upvotes pyq_upvote[]
+ course_review_upvotes course_review_upvote[]
}
model subject {
@@ -42,6 +43,7 @@ model instructor {
updated_at DateTime @updatedAt
notes note[]
pyqs pyq[]
+ course course[]
}
model note {
@@ -75,25 +77,43 @@ model note_upvote {
model course {
id Int @id @unique @default(autoincrement())
title String @db.VarChar(255)
- code String @unique @db.VarChar(100)
+ instructor instructor @relation(fields: [instructor_id], references: [id], onDelete: Restrict, onUpdate: Cascade)
+ instructor_id Int
+ code String @db.VarChar(100)
anonymous Boolean
created_by user @relation(fields: [created_by_id], references: [user_id], onDelete: Restrict)
created_by_id String
created_at DateTime @default(now())
updated_at DateTime @updatedAt
- upvotes course_upvote[]
+ reviews course_review[]
+
+ @@unique([instructor_id, code])
}
-model course_upvote {
- id Int @id @unique @default(autoincrement())
- user user @relation(fields: [user_id], references: [user_id], onDelete: Cascade, onUpdate: Cascade)
+model course_review {
+ id Int @id @unique @default(autoincrement())
+ user user @relation(fields: [user_id], references: [user_id], onDelete: Cascade, onUpdate: Cascade)
user_id String
- course course @relation(fields: [course_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ course course @relation(fields: [course_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
course_id Int
+ rating Float @default(0.0)
+ comment String
+ anonymous Boolean
+ upvotes course_review_upvote[]
@@unique([user_id, course_id])
}
+model course_review_upvote {
+ id Int @id @unique @default(autoincrement())
+ user user @relation(fields: [user_id], references: [user_id], onDelete: Cascade, onUpdate: Cascade)
+ user_id String
+ course_review course_review @relation(fields: [course_review_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ course_review_id Int
+
+ @@unique([user_id, course_review_id])
+}
+
model pyq {
id Int @id @unique @default(autoincrement())
title String @db.VarChar(255)
diff --git a/src/components/Common/ModalContainer.tsx b/src/components/Common/ModalContainer.tsx
index 6f3590fa..1121c45e 100644
--- a/src/components/Common/ModalContainer.tsx
+++ b/src/components/Common/ModalContainer.tsx
@@ -13,7 +13,7 @@ export const ModalContainer: FC<{
!showModal && 'hidden'
} flex justify-center items-center fixed top-0 left-0 right-0 z-50 w-full bg-black/50 overflow-x-hidden overflow-y-auto h-full`}
>
-
+
diff --git a/src/components/Courses/Modal.tsx b/src/components/Courses/Modal.tsx
index 4ad3f0a9..198d15df 100644
--- a/src/components/Courses/Modal.tsx
+++ b/src/components/Courses/Modal.tsx
@@ -2,6 +2,9 @@ import React, { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'
import { toast } from 'react-hot-toast'
import { ModalContainer } from '../Common/ModalContainer'
import { Input } from '../Common/Input'
+import { SelectInput } from '../Common/SelectInput'
+import { getInstructors } from '../../services/db/instructors/getInstructors'
+import { addInstructor } from '../../services/db/instructors/addInstructor'
export const Modal: FC<{
isUpdateModal?: boolean
@@ -27,23 +30,54 @@ export const Modal: FC<{
const [title, setTitle] = useState('')
const [code, setCode] = useState('')
const [isAnonymous, setIsAnonymous] = useState(false)
+ const [instructors, setInstructors] = useState([])
+
+ const [instructorNameInput, setInstructorNameInput] = useState('')
+ const [selectedInstructorId, setSelectedInstructorId] = useState(0)
+ const [showInstructorNameDropdown, setShowInstructorNameDropdown] =
+ useState(false)
+ const [selectedInstructorName, setSelectedInstructorName] =
+ useState('')
//? functions
const actionHandler = async (e: any) => {
e.preventDefault()
- if (title === '' || code === '') {
+ if (
+ title === '' ||
+ code === '' ||
+ (selectedInstructorId === 0 && selectedInstructorName === '')
+ ) {
toast.error('Please fill all the details!')
} else {
setIsLoading(true)
- await actionFunction({
- id: isUpdateModal ? selectedEntity.id : null,
- title: title,
- code: code,
- isAnonymous: isAnonymous,
- refetch: refetch,
- })
+ if (
+ !instructors.find(
+ (instructor) => instructor.name === selectedInstructorName
+ )
+ ) {
+ const instructor = await addInstructor(selectedInstructorName)
+ await actionFunction({
+ id: isUpdateModal ? selectedEntity.id : null,
+ title: title,
+ code: code,
+ instructorId: instructor.id,
+ isAnonymous: isAnonymous,
+ refetch: refetch,
+ })
+ } else {
+ await actionFunction({
+ id: isUpdateModal ? selectedEntity.id : null,
+ title: title,
+ code: code,
+ instructorId: selectedInstructorId,
+ isAnonymous: isAnonymous,
+ refetch: refetch,
+ })
+ }
setTitle('')
setCode('')
+ setSelectedInstructorName('')
+ setSelectedInstructorId(0)
setIsAnonymous(false)
setShowModal(false)
setIsLoading(false)
@@ -55,6 +89,7 @@ export const Modal: FC<{
if (selectedEntity) {
setTitle(selectedEntity.title)
setCode(selectedEntity.code)
+ setSelectedInstructorName(selectedEntity.instructor.name)
setIsAnonymous(selectedEntity.anonymous)
}
}, [selectedEntity])
@@ -71,6 +106,17 @@ export const Modal: FC<{
document.removeEventListener('keydown', keyPressHandler, false)
}, [setShowModal])
+ useEffect(() => {
+ getInstructors().then((res) => setInstructors(res))
+ }, [])
+
+ useEffect(() => {
+ instructors.map((instructor) => {
+ if (instructor.name === selectedInstructorName)
+ setSelectedInstructorId(instructor.id)
+ })
+ }, [selectedInstructorName, instructors])
+
return (
{/* Title */}
-
+ {/* Code */}
+ {/* Instructor */}
+ {/* Instructor */}
+
{/* Anonymous */}
diff --git a/src/components/Courses/Reviews/Modal.tsx b/src/components/Courses/Reviews/Modal.tsx
new file mode 100644
index 00000000..16398dd4
--- /dev/null
+++ b/src/components/Courses/Reviews/Modal.tsx
@@ -0,0 +1,139 @@
+import React, { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'
+import ReactStars from 'react-stars'
+import { toast } from 'react-hot-toast'
+import { ModalContainer } from '../../Common/ModalContainer'
+
+export const Modal: FC<{
+ isUpdateModal?: boolean
+ header: string
+ courseId: number
+ actionButtonText: string
+ actionFunction: Function
+ showModal: boolean
+ refetch: Function
+ setShowModal: Dispatch>
+ selectedEntity?: any
+}> = ({
+ header,
+ courseId,
+ refetch,
+ showModal,
+ actionButtonText,
+ actionFunction,
+ setShowModal,
+ selectedEntity,
+ isUpdateModal = false,
+}) => {
+ //? states
+ const [isLoading, setIsLoading] = useState(false)
+ const [comment, setComment] = useState('')
+ const [rating, setRating] = useState(0)
+ const [isAnonymous, setIsAnonymous] = useState(false)
+
+ //? functions
+ const actionHandler = async (e: any) => {
+ e.preventDefault()
+ if (comment === '' || rating === 0) {
+ toast.error('Please fill all the details!')
+ } else {
+ setIsLoading(true)
+ actionFunction({
+ id: isUpdateModal ? selectedEntity.id : null,
+ courseId,
+ comment,
+ rating,
+ isAnonymous,
+ refetch,
+ })
+ setRating(0)
+ setComment('')
+ setIsAnonymous(false)
+ setShowModal(false)
+ setIsLoading(false)
+ }
+ }
+
+ const onRatingChangeHandler = (newRating: any) => {
+ setRating(newRating)
+ }
+
+ //? effects
+ useEffect(() => {
+ if (selectedEntity) {
+ setComment(selectedEntity.comment)
+ setRating(selectedEntity.rating)
+ setIsAnonymous(selectedEntity.anonymous)
+ }
+ }, [selectedEntity])
+
+ useEffect(() => {
+ const keyPressHandler = (event: any) => {
+ if (event.key === 'Escape') {
+ setShowModal(false)
+ }
+ }
+ document.addEventListener('keydown', keyPressHandler, false)
+
+ return () =>
+ document.removeEventListener('keydown', keyPressHandler, false)
+ }, [setShowModal])
+
+ return (
+
+
+ {/* Rating */}
+
+ Rating:
+
+
+
+ {/* Title */}
+
+ Comment
+
+
+ {/* Anonymous */}
+
+
+ Do you want it post as Anonymous?
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/pages/api/db/courses/index.ts b/src/pages/api/db/courses/index.ts
index 22ccc1f1..87159cb6 100644
--- a/src/pages/api/db/courses/index.ts
+++ b/src/pages/api/db/courses/index.ts
@@ -12,29 +12,89 @@ export default async function coursesHandler(
switch (method) {
case 'GET':
try {
- const courses = await prisma.course.findMany({
- include: {
- created_by: {
- select: {
- name: true,
- },
+ if (query.id) {
+ const course = await prisma.course.findUnique({
+ where: {
+ id: parseInt(query.id as string),
},
- upvotes: {
- select: {
- user_id: true,
+ select: {
+ id: true,
+ title: true,
+ code: true,
+ anonymous: true,
+ instructor_id: true,
+ created_by_id: true,
+ _count: {
+ select: {
+ reviews: true,
+ },
+ },
+ reviews: {
+ select: {
+ comment: true,
+ user: {
+ select: {
+ name: true,
+ },
+ },
+ upvotes: {
+ select: {
+ user_id: true,
+ },
+ },
+ anonymous: true,
+ user_id: true,
+ rating: true,
+ id: true,
+ _count: {
+ select: {
+ upvotes: true,
+ },
+ },
+ },
+ },
+ created_by: {
+ select: {
+ name: true,
+ },
+ },
+ instructor: {
+ select: {
+ name: true,
+ },
},
},
- _count: {
- select: {
- upvotes: true,
+ })
+
+ res.status(200).json({
+ message: 'Course Fetched',
+ result: course,
+ })
+ } else {
+ const courses = await prisma.course.findMany({
+ include: {
+ created_by: {
+ select: {
+ name: true,
+ },
+ },
+ instructor: {
+ select: {
+ name: true,
+ },
+ },
+ _count: {
+ select: {
+ reviews: true,
+ },
},
},
- },
- })
- res.status(200).json({
- message: 'Courses Fetched',
- result: courses,
- })
+ })
+ res.status(200).json({
+ message: 'Courses Fetched',
+ result: courses,
+ })
+ }
} catch (err: any) {
res.status(404).json({
message: err,
@@ -47,10 +107,13 @@ export default async function coursesHandler(
const user = await adminAuth.verifyIdToken(accessToken!)
if (user) {
- const { title, code, isAnonymous } = body
+ const { title, code, instructorId, isAnonymous } = body
const course = await prisma.course.findUnique({
where: {
- code: code,
+ instructor_id_code: {
+ code: code,
+ instructor_id: instructorId,
+ },
},
})
@@ -64,6 +127,7 @@ export default async function coursesHandler(
data: {
title: title,
code: code,
+ instructor_id: instructorId,
anonymous: isAnonymous,
created_by_id: user.user_id,
},
@@ -96,7 +160,7 @@ export default async function coursesHandler(
if (user) {
const { id } = query
- const { title, code, isAnonymous } = body
+ const { title, code, instructorId, isAnonymous } = body
try {
await prisma.course.update({
@@ -106,6 +170,7 @@ export default async function coursesHandler(
data: {
title: title,
code: code,
+ instructor_id: instructorId,
anonymous: isAnonymous,
},
})
diff --git a/src/pages/api/db/courses/review.ts b/src/pages/api/db/courses/review.ts
new file mode 100644
index 00000000..fd58f0e7
--- /dev/null
+++ b/src/pages/api/db/courses/review.ts
@@ -0,0 +1,137 @@
+import type { NextApiRequest, NextApiResponse } from 'next'
+import { adminAuth } from '../../../../utils/firebaseAdminInit'
+import { prisma } from '../../../../utils/prismaClientInit'
+
+export default async function courseReviewHandler(
+ req: NextApiRequest,
+ res: NextApiResponse
+) {
+ const { method, headers, body, query } = req
+
+ switch (method) {
+ case 'POST':
+ if (headers && headers.authorization) {
+ const accessToken = headers.authorization.split(' ')[1]
+ const user = await adminAuth.verifyIdToken(accessToken!)
+
+ if (user) {
+ try {
+ const { user_id } = user
+ const { course_id, comment, rating, isAnonymous } = body
+
+ await prisma.course_review.create({
+ data: {
+ course_id,
+ comment,
+ rating,
+ user_id,
+ anonymous: isAnonymous,
+ },
+ })
+
+ res.status(200).json({
+ message: 'Review Added',
+ })
+ } catch (err: any) {
+ console.log(err)
+ res.status(405).json({
+ err,
+ })
+ }
+ } else {
+ res.status(401).json({
+ message: 'Unauthorized Access',
+ })
+ }
+ } else {
+ res.status(401).json({
+ message: 'Unauthorized Access',
+ })
+ }
+ break
+ case 'PUT':
+ if (headers && headers.authorization) {
+ const accessToken = headers.authorization.split(' ')[1]
+ const user = await adminAuth.verifyIdToken(accessToken!)
+
+ if (user) {
+ try {
+ const { user_id } = user
+ const { id } = query
+ const { comment, isAnonymous, rating } = body
+
+ await prisma.course_review.updateMany({
+ where: {
+ id: parseInt(id as string),
+ user_id: user_id,
+ },
+ data: {
+ comment,
+ rating,
+ anonymous: isAnonymous,
+ },
+ })
+
+ res.status(200).json({
+ message: 'Review Updated',
+ })
+ } catch (err: any) {
+ console.log(err)
+ res.status(405).json({
+ err,
+ })
+ }
+ } else {
+ res.status(401).json({
+ message: 'Unauthorized Access',
+ })
+ }
+ } else {
+ res.status(401).json({
+ message: 'Unauthorized Access',
+ })
+ }
+ break
+ case 'DELETE':
+ if (headers && headers.authorization) {
+ const accessToken = headers.authorization.split(' ')[1]
+ const user = await adminAuth.verifyIdToken(accessToken!)
+
+ if (user) {
+ try {
+ const { user_id } = user
+ const { id } = query
+
+ await prisma.course_review.deleteMany({
+ where: {
+ user_id,
+ id: parseInt(id as string),
+ },
+ })
+
+ res.status(200).json({
+ message: 'Review Deleted',
+ })
+ } catch (err: any) {
+ console.log(err)
+ res.status(405).json({
+ err,
+ })
+ }
+ } else {
+ res.status(401).json({
+ message: 'Unauthorized Access',
+ })
+ }
+ } else {
+ res.status(401).json({
+ message: 'Unauthorized Access',
+ })
+ }
+ break
+ default:
+ res.status(405).json({
+ message: 'Method Not Allowed',
+ })
+ }
+}
diff --git a/src/pages/api/db/courses/upvote.ts b/src/pages/api/db/courses/review_upvote.ts
similarity index 83%
rename from src/pages/api/db/courses/upvote.ts
rename to src/pages/api/db/courses/review_upvote.ts
index cf6f0cc1..e4102746 100644
--- a/src/pages/api/db/courses/upvote.ts
+++ b/src/pages/api/db/courses/review_upvote.ts
@@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'
import { adminAuth } from '../../../../utils/firebaseAdminInit'
import { prisma } from '../../../../utils/prismaClientInit'
-export default async function courseUpvoteHandler(
+export default async function courseReviewUpvoteHandler(
req: NextApiRequest,
res: NextApiResponse
) {
@@ -17,11 +17,11 @@ export default async function courseUpvoteHandler(
if (user) {
try {
const { user_id } = user
- const { course_id } = body
+ const { course_review_id } = body
- await prisma.course_upvote.create({
+ await prisma.course_review_upvote.create({
data: {
- course_id,
+ course_review_id,
user_id,
},
})
@@ -56,15 +56,15 @@ export default async function courseUpvoteHandler(
const { user_id } = user
const { id } = query
- await prisma.course_upvote.deleteMany({
+ await prisma.course_review_upvote.deleteMany({
where: {
+ course_review_id: parseInt(id as string),
user_id,
- course_id: parseInt(id as string),
},
})
res.status(200).json({
- message: 'Upvote Removed',
+ message: 'Removed Upvote',
})
} catch (err: any) {
console.log(err)
@@ -83,9 +83,5 @@ export default async function courseUpvoteHandler(
})
}
break
- default:
- res.status(405).json({
- message: 'Method Not Allowed',
- })
}
}
diff --git a/src/pages/courses.tsx b/src/pages/courses.tsx
deleted file mode 100644
index 3a4056da..00000000
--- a/src/pages/courses.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import Head from 'next/head'
-import { BsPlus } from 'react-icons/bs'
-import { useAuth } from '../contexts/auth'
-import { useEffect, useState } from 'react'
-import { coursesColumnData } from '../types/coursesColumnData'
-import { toast } from 'react-hot-toast'
-import { Table } from '../components/Courses/Table'
-import { getCourses } from '../services/db/courses/getCourses'
-import { Modal } from '../components/Courses/Modal'
-import { addCourse } from '../services/db/courses/addCourse'
-import { updateCourse } from '../services/db/courses/updateCourse'
-
-export default function Courses() {
- //? contexts
- const { user, loading }: any = useAuth()
-
- //? states
- const [isDataFetching, setIsDataFetching] = useState(false)
- const [showAddCourseModal, setShowAddCourseModal] = useState(false)
- const [showUpdateCourseModal, setShowUpdateCourseModal] =
- useState(false)
- const [selectedCourse, setSelectedCourse] = useState(null)
- const [courses, setCourses] = useState([])
-
- //? functions
- const refetchCourses = () => {
- setIsDataFetching(true)
- getCourses().then((res) => {
- setCourses(res)
- setIsDataFetching(false)
- })
- }
-
- //? effects
- useEffect(() => {
- setIsDataFetching(true)
- getCourses().then((res) => {
- setCourses(res)
- setIsDataFetching(false)
- })
- }, [])
-
- useEffect(() => {
- if (selectedCourse) setShowUpdateCourseModal(true)
- else setShowUpdateCourseModal(false)
- }, [selectedCourse])
-
- return (
-
- {user && (
-
- )}
- {selectedCourse && (
-
- )}
-
-
Courses
-
-
-
-
-
-
-
Courses
-
-
-
-
-
- )
-}
diff --git a/src/pages/courses/[...id].tsx b/src/pages/courses/[...id].tsx
new file mode 100644
index 00000000..83f8e429
--- /dev/null
+++ b/src/pages/courses/[...id].tsx
@@ -0,0 +1,306 @@
+import { NextPage } from 'next'
+import { useEffect, useState } from 'react'
+import { getCourseById } from '../../services/db/courses/getCourseById'
+import { useRouter } from 'next/router'
+import Head from 'next/head'
+import { BsPencilSquare, BsPlus } from 'react-icons/bs'
+import { useAuth } from '../../contexts/auth'
+import { toast } from 'react-hot-toast'
+import { Modal } from '../../components/Courses/Reviews/Modal'
+import { addCourseReview } from '../../services/db/courses/courseReview/addCourseReview'
+import { updateCourseReview } from '../../services/db/courses/courseReview/updateCourseReview'
+import ReactStars from 'react-stars'
+import { RiDeleteBin6Line } from 'react-icons/ri'
+import { TiTick } from 'react-icons/ti'
+import { deleteCourseReview } from '../../services/db/courses/courseReview/deleteCourseReview'
+import { Player } from '@lottiefiles/react-lottie-player'
+import { UpvoteButton } from '../../components/Common/UpvoteButton'
+import { removeCourseReviewUpvote } from '../../services/db/courses/courseReview/courseReviewUpvote/removeUpvote'
+import { upvoteCourseReview } from '../../services/db/courses/courseReview/courseReviewUpvote/upvote'
+
+const Course: NextPage = ({}) => {
+ //? router
+ const router = useRouter()
+
+ //? states
+ const [course, setCourse] = useState(null)
+ const [reviews, setReviews] = useState([])
+ const [selectedCourseReview, setSelectedCourseReview] = useState(null)
+ const [isDataFetching, setIsDataFetching] = useState(false)
+ const [alreadyReviewed, setAlreadyReviewed] = useState(false)
+ const [showAddReviewModal, setShowAddReviewModal] = useState(false)
+ const [showUpdateReviewModal, setShowUpdateReviewModal] =
+ useState(false)
+
+ //? contexts
+ const { user, loading }: any = useAuth()
+
+ //? functions
+ const sortReviews = (reviews: Array) => {
+ reviews.sort((reviewA: any, reviewB: any) =>
+ reviewA.upvotes.length > reviewB.upvotes.length ? -1 : 1
+ )
+ if (user) {
+ const userReviews = reviews.filter(
+ (review) => user.user_id === review.user_id
+ )
+ const restReviews = reviews.filter(
+ (review) => !(user.user_id === review.user_id)
+ )
+
+ return [...userReviews, ...restReviews]
+ } else return reviews
+ }
+
+ const refetchReviews = () => {
+ setIsDataFetching(true)
+ if (router.query.id) {
+ const id = parseInt(router.query.id[0])
+ getCourseById({ id }).then((res) => {
+ setReviews(res.reviews)
+ setIsDataFetching(false)
+ })
+ }
+ }
+
+ //? effects
+ useEffect(() => {
+ setIsDataFetching(true)
+ if (router.query.id) {
+ const id = parseInt(router.query.id[0])
+ getCourseById({ id }).then((res) => {
+ setCourse(res)
+ setReviews(res.reviews)
+ setIsDataFetching(false)
+ })
+ }
+ }, [router])
+
+ useEffect(() => {
+ if (selectedCourseReview) setShowUpdateReviewModal(true)
+ else setShowUpdateReviewModal(false)
+ }, [selectedCourseReview])
+
+ useEffect(() => {
+ if (user && reviews)
+ reviews.find((review) => user.user_id === review.user_id)
+ ? setAlreadyReviewed(true)
+ : setAlreadyReviewed(false)
+ }, [reviews, user])
+
+ return (
+ course && (
+
+
+
Course
+
+
+
+
+ {user && (
+
+ )}
+ {selectedCourseReview && (
+
+ )}
+
+
+
+
+ Course:
+
+
+ {course.title}
+
+
+ {alreadyReviewed ? (
+
+ ) : (
+
+ )}
+
+
+ Reviews
+
+
+ {isDataFetching ? (
+ Array(8)
+ .fill({})
+ .map((res, index) => {
+ return (
+
+
+
+
+
+
+
+
+ Loading...
+
+
+ )
+ })
+ ) : reviews.length ? (
+ sortReviews(reviews).map((review: any) => {
+ return (
+
+
+
+
+ {user &&
+ user.user_id ===
+ review.user_id && (
+
+
+
+
+ )}
+
+
+
+ Review:
+
+
{review.comment}
+
+
+
+ {review.anonymous ? (
+
+ ~ Anonymous
+
+ ) : (
+
+ ~ {review.user.name}
+
+ )}
+
+
+
+ )
+ })
+ ) : (
+
+
+
+ No Reviews Found
+
+
+ )}
+
+
+
+ )
+ )
+}
+
+export default Course
diff --git a/src/pages/courses/index.tsx b/src/pages/courses/index.tsx
new file mode 100644
index 00000000..b3307b93
--- /dev/null
+++ b/src/pages/courses/index.tsx
@@ -0,0 +1,267 @@
+import Head from 'next/head'
+import { BsPencilSquare, BsPlus } from 'react-icons/bs'
+import { useAuth } from '../../contexts/auth'
+import { useEffect, useState } from 'react'
+import { coursesColumnData } from '../../types/coursesColumnData'
+import { toast } from 'react-hot-toast'
+import { getCourses } from '../../services/db/courses/getCourses'
+import { Modal } from '../../components/Courses/Modal'
+import { addCourse } from '../../services/db/courses/addCourse'
+import { updateCourse } from '../../services/db/courses/updateCourse'
+import { Player } from '@lottiefiles/react-lottie-player'
+import { useRouter } from 'next/router'
+import { RiDeleteBin6Line } from 'react-icons/ri'
+import { deleteCourse } from '../../services/db/courses/deleteCourse'
+
+export default function Courses() {
+ //? router
+ const router = useRouter()
+
+ //? contexts
+ const { user, loading }: any = useAuth()
+
+ //? states
+ const [isDataFetching, setIsDataFetching] = useState(false)
+ const [showAddCourseModal, setShowAddCourseModal] = useState(false)
+ const [showUpdateCourseModal, setShowUpdateCourseModal] =
+ useState(false)
+ const [selectedCourse, setSelectedCourse] = useState(null)
+ const [courses, setCourses] = useState([])
+ const [searchInput, setSearchInput] = useState('')
+
+ //? functions
+ const refetchCourses = () => {
+ setIsDataFetching(true)
+ getCourses().then((res) => {
+ setCourses(res)
+ setIsDataFetching(false)
+ })
+ }
+
+ //? effects
+ useEffect(() => {
+ setIsDataFetching(true)
+ getCourses().then((res) => {
+ setCourses(res)
+ setIsDataFetching(false)
+ })
+ }, [])
+
+ useEffect(() => {
+ if (selectedCourse) setShowUpdateCourseModal(true)
+ else setShowUpdateCourseModal(false)
+ }, [selectedCourse])
+
+ return (
+
+ {user && (
+
+ )}
+ {selectedCourse && (
+
+ )}
+
+
Courses
+
+
+
+
+
+
+
Courses
+
+
+
+
+
+
+
{
+ setSearchInput(e.target.value)
+ }}
+ type="search"
+ id="search"
+ className="w-full font-medium text-base pl-12 pr-2 py-2 md:pr-4 md:py-4 text-gray-700 outline-none ring-2 ring-primary/40 focus:border-none rounded-md bg-primary/5 focus:bg-primary/10 focus:ring-primary placeholder:font-medium placeholder:text-gray-600"
+ placeholder={`Search ${
+ courses && courses.length
+ } records...`}
+ />
+
+
+
+ {isDataFetching ? (
+ Array(8)
+ .fill({})
+ .map((res, index) => {
+ return (
+
+
+
+
+
+
+
+
+ Loading...
+
+
+ )
+ })
+ ) : courses.length ? (
+ courses
+ .filter((course: any) => {
+ const regex = new RegExp(searchInput, 'i')
+ console.log(regex)
+
+ if (course.title.match(regex)) return true
+ else return false
+ })
+ .map((course: any) => {
+ return (
+
+
+
+ router.push(
+ '/courses/' + course.id
+ )
+ }
+ className="font-semibold cursor-pointer text-primary hover:underline duration-150 transition-all text-xl"
+ >
+ {course.title}
+
+ {user &&
+ user.user_id ===
+ course.created_by_id && (
+
+
+
+
+ )}
+
+
+
+ Course Code:
+
+
{course.code}
+
+
+
+ Instructor:
+
+
{course.instructor.name}
+
+
+
+ Total Reviews:
+
+
{course._count.reviews}
+
+
+ )
+ })
+ ) : (
+
+
+
+ No Records Found
+
+
+ )}
+
+
+
+ )
+}
diff --git a/src/pages/notes.tsx b/src/pages/notes.tsx
index 570471e3..93a80870 100644
--- a/src/pages/notes.tsx
+++ b/src/pages/notes.tsx
@@ -24,7 +24,6 @@ export default function Notes() {
const [selectedNote, setSelectedNote] = useState(null)
const [notes, setNotes] = useState([])
- console.log(notes)
//? functions
const refetchNotes = () => {
setIsDataFetching(true)
diff --git a/src/services/db/courses/addCourse.ts b/src/services/db/courses/addCourse.ts
index 434e9495..e7baabf5 100644
--- a/src/services/db/courses/addCourse.ts
+++ b/src/services/db/courses/addCourse.ts
@@ -4,15 +4,23 @@ import { api } from '../../../utils/api'
interface Props {
title: string
code: string
+ instructorId: number
isAnonymous: boolean
refetch: Function
}
-export const addCourse = ({ title, code, isAnonymous, refetch }: Props) => {
+export const addCourse = ({
+ title,
+ code,
+ instructorId,
+ isAnonymous,
+ refetch,
+}: Props) => {
toast.promise(
api.post('/api/db/courses', {
title,
code,
+ instructorId,
isAnonymous,
}),
{
diff --git a/src/services/db/courses/courseReview/addCourseReview.ts b/src/services/db/courses/courseReview/addCourseReview.ts
new file mode 100644
index 00000000..1f86a3f6
--- /dev/null
+++ b/src/services/db/courses/courseReview/addCourseReview.ts
@@ -0,0 +1,35 @@
+import { toast } from 'react-hot-toast'
+import { api } from '../../../../utils/api'
+
+interface Props {
+ courseId: number
+ comment: string
+ rating: number
+ isAnonymous: boolean
+ refetch: Function
+}
+
+export const addCourseReview = ({
+ courseId,
+ comment,
+ isAnonymous,
+ rating,
+ refetch,
+}: Props) => {
+ toast.promise(
+ api.post('/api/db/courses/review', {
+ course_id: courseId,
+ comment,
+ isAnonymous,
+ rating,
+ }),
+ {
+ loading: 'Loading...',
+ success: (res) => {
+ refetch()
+ return `${res.data.message}`
+ },
+ error: (err) => `Error: ${err.message}`,
+ }
+ )
+}
diff --git a/src/services/db/courses/courseReview/courseReviewUpvote/removeUpvote.ts b/src/services/db/courses/courseReview/courseReviewUpvote/removeUpvote.ts
new file mode 100644
index 00000000..eeb5d46f
--- /dev/null
+++ b/src/services/db/courses/courseReview/courseReviewUpvote/removeUpvote.ts
@@ -0,0 +1,27 @@
+import { Dispatch, SetStateAction } from 'react'
+import { api } from '../../../../../utils/api'
+import { toast } from 'react-hot-toast'
+
+interface Props {
+ id: number
+ setCount: Dispatch>
+ setIsUpvoted: Dispatch>
+}
+
+export const removeCourseReviewUpvote = ({
+ id,
+ setCount,
+ setIsUpvoted,
+}: Props) => {
+ toast.promise(api.delete('/api/db/courses/review_upvote?id=' + id), {
+ loading: 'Loading...',
+ success: (res) => {
+ setCount((prevCount: any) => {
+ return prevCount - 1
+ })
+ setIsUpvoted(false)
+ return `${res.data.message}`
+ },
+ error: (err) => `Error: ${err.message}`,
+ })
+}
diff --git a/src/services/db/courses/courseReview/courseReviewUpvote/upvote.ts b/src/services/db/courses/courseReview/courseReviewUpvote/upvote.ts
new file mode 100644
index 00000000..96f45547
--- /dev/null
+++ b/src/services/db/courses/courseReview/courseReviewUpvote/upvote.ts
@@ -0,0 +1,28 @@
+import { Dispatch, SetStateAction } from 'react'
+import { toast } from 'react-hot-toast'
+import { api } from '../../../../../utils/api'
+
+interface Props {
+ id: number
+ setCount: Dispatch>
+ setIsUpvoted: Dispatch>
+}
+
+export const upvoteCourseReview = ({ id, setCount, setIsUpvoted }: Props) => {
+ toast.promise(
+ api.post('/api/db/courses/review_upvote', {
+ course_review_id: id,
+ }),
+ {
+ loading: 'Loading...',
+ success: (res) => {
+ setCount((prevCount: any) => {
+ return prevCount + 1
+ })
+ setIsUpvoted(true)
+ return `${res.data.message}`
+ },
+ error: (err) => `Error: ${err.message}`,
+ }
+ )
+}
diff --git a/src/services/db/courses/courseReview/deleteCourseReview.ts b/src/services/db/courses/courseReview/deleteCourseReview.ts
new file mode 100644
index 00000000..7c3c3007
--- /dev/null
+++ b/src/services/db/courses/courseReview/deleteCourseReview.ts
@@ -0,0 +1,18 @@
+import { toast } from 'react-hot-toast'
+import { api } from '../../../../utils/api'
+
+interface Props {
+ id: number
+ refetch: Function
+}
+
+export const deleteCourseReview = ({ id, refetch }: Props) => {
+ toast.promise(api.delete('/api/db/courses/review?id=' + id), {
+ loading: 'Loading...',
+ success: (res) => {
+ refetch()
+ return `${res.data.message}`
+ },
+ error: (err) => `Error: ${err.message}`,
+ })
+}
diff --git a/src/services/db/courses/courseReview/updateCourseReview.ts b/src/services/db/courses/courseReview/updateCourseReview.ts
new file mode 100644
index 00000000..cad77044
--- /dev/null
+++ b/src/services/db/courses/courseReview/updateCourseReview.ts
@@ -0,0 +1,34 @@
+import { toast } from 'react-hot-toast'
+import { api } from '../../../../utils/api'
+
+interface Props {
+ id: number
+ comment: string
+ rating: number
+ isAnonymous: boolean
+ refetch: Function
+}
+
+export const updateCourseReview = ({
+ id,
+ comment,
+ rating,
+ isAnonymous,
+ refetch,
+}: Props) => {
+ toast.promise(
+ api.put('/api/db/courses/review?id=' + id, {
+ comment,
+ isAnonymous,
+ rating,
+ }),
+ {
+ loading: 'Loading...',
+ success: (res) => {
+ refetch()
+ return `${res.data.message}`
+ },
+ error: (err) => `Error: ${err.message}`,
+ }
+ )
+}
diff --git a/src/services/db/courses/getCourseById.ts b/src/services/db/courses/getCourseById.ts
new file mode 100644
index 00000000..c7976afc
--- /dev/null
+++ b/src/services/db/courses/getCourseById.ts
@@ -0,0 +1,15 @@
+import { toast } from 'react-hot-toast'
+import { api } from '../../../utils/api'
+
+interface Props {
+ id: number
+}
+
+export const getCourseById = async ({ id }: Props) => {
+ try {
+ const { data } = await api.get('/api/db/courses?id=' + id)
+ return data.result
+ } catch (err: any) {
+ toast.error(err.message)
+ }
+}