From b2905a7641929499b2492b1c815185c7e55ffde4 Mon Sep 17 00:00:00 2001 From: Subin S K Date: Sat, 14 Jan 2023 21:35:12 +0530 Subject: [PATCH 1/5] courses ui done --- package-lock.json | 30 ++ package.json | 2 + prisma/schema.prisma | 18 +- src/components/Common/ModalContainer.tsx | 2 +- src/components/Courses/Modal.tsx | 81 ++++- src/components/Courses/Reviews/Modal.tsx | 139 +++++++++ src/pages/api/db/courses/index.ts | 92 ++++-- .../api/db/courses/{upvote.ts => review.ts} | 58 +++- src/pages/courses.tsx | 117 -------- src/pages/courses/[...id].tsx | 278 ++++++++++++++++++ src/pages/courses/index.tsx | 219 ++++++++++++++ src/pages/notes.tsx | 1 - src/services/db/courses/addCourse.ts | 10 +- .../courses/courseReview/addCourseReview.ts | 35 +++ .../courseReview/deleteCourseReview.ts | 18 ++ .../courseReview/updateCourseReview.ts | 34 +++ src/services/db/courses/getCourseById.ts | 15 + 17 files changed, 988 insertions(+), 161 deletions(-) create mode 100644 src/components/Courses/Reviews/Modal.tsx rename src/pages/api/db/courses/{upvote.ts => review.ts} (56%) delete mode 100644 src/pages/courses.tsx create mode 100644 src/pages/courses/[...id].tsx create mode 100644 src/pages/courses/index.tsx create mode 100644 src/services/db/courses/courseReview/addCourseReview.ts create mode 100644 src/services/db/courses/courseReview/deleteCourseReview.ts create mode 100644 src/services/db/courses/courseReview/updateCourseReview.ts create mode 100644 src/services/db/courses/getCourseById.ts 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..13edf05a 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,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..82938ac4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -17,7 +17,7 @@ model user { pyqs pyq[] instructors instructor[] note_upvotes note_upvote[] - course_upvotes course_upvote[] + course_reviews course_review[] pyq_upvotes pyq_upvote[] } @@ -42,6 +42,7 @@ model instructor { updated_at DateTime @updatedAt notes note[] pyqs pyq[] + course course[] } model note { @@ -75,21 +76,26 @@ model note_upvote { model course { id Int @id @unique @default(autoincrement()) title String @db.VarChar(255) + instructor instructor @relation(fields: [instructor_id], references: [id], onDelete: Restrict, onUpdate: Cascade) + instructor_id Int code String @unique @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[] } -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 @@unique([user_id, course_id]) } 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..3e4280dc 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 ( - + {/* 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 +