From 88080f56b63686785955914da02de3470f3f6b44 Mon Sep 17 00:00:00 2001 From: Alec Date: Wed, 18 Jun 2025 09:46:42 +1000 Subject: [PATCH 1/3] Update code to match main --- .DS_Store | Bin 0 -> 6148 bytes backend/.gitignore | 3 +- backend/package-lock.json | 177 +++- backend/package.json | 8 +- backend/prisma/migrations/migration_lock.toml | 3 + .../migration.sql | 19 + backend/prisma/schema.prisma | 58 +- backend/src/api/schemas/review.schema.ts | 49 + backend/src/controllers/course.controller.ts | 18 + backend/src/controllers/review.controller.ts | 82 +- backend/src/repositories/course.repository.ts | 70 +- backend/src/repositories/review.repository.ts | 46 +- backend/src/services/course.service.ts | 11 + backend/src/services/review.service.test.ts | 4 + backend/src/services/review.service.ts | 90 +- backend/tsconfig.json | 7 +- frontend/package-lock.json | 969 +++++++++++++++++- frontend/package.json | 1 + frontend/src/app/course/[id]/page.tsx | 13 +- frontend/src/app/page.tsx | 1 + .../EditReviewModal/EditReviewModal.tsx | 4 +- .../components/FilterModal/FilterModal.tsx | 9 - .../src/components/ReviewCard/ReviewCard.tsx | 26 +- .../components/ReviewModal/ReviewModal.tsx | 8 - .../src/components/SearchBar/SearchBar.tsx | 4 - .../SubcomRecruitmentPopup.tsx | 31 + .../TruncatedDescription.tsx | 10 +- .../UserBookmarkedReviews.tsx | 14 +- .../UserPageContent/UserPageContent.tsx | 6 +- .../components/UserReports/UserReports.tsx | 15 +- .../components/UserReviews/UserReviews.tsx | 30 +- frontend/src/types/api.ts | 20 +- scraper/.gitignore | 2 + scraper/studentVIP.py | 56 + scraper/uniNotes.py | 77 ++ 35 files changed, 1785 insertions(+), 156 deletions(-) create mode 100644 .DS_Store create mode 100644 backend/prisma/migrations/migration_lock.toml create mode 100644 backend/prisma/migrations/z0_20241104062037_add_reviews_scraped/migration.sql create mode 100644 frontend/src/components/SubcomRecruitmentPopup/SubcomRecruitmentPopup.tsx create mode 100644 scraper/.gitignore create mode 100644 scraper/studentVIP.py create mode 100644 scraper/uniNotes.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..93a2aa01fc027d1924d61ff67fcf1f78e940d5ac GIT binary patch literal 6148 zcmeHKJ8nWj3>+s&K}thOxmVx@D@0C^3nT$LBoHY5t8y-mmhneX(1R*UgT|6QyMCUx z+9{r&0od|$vjAoQrgTSqc^I2NcOTh9WsFGYI}X_5xH+wNANx`D^@MYuaKai#y#MC! zch?EZN&zV#1*Cu!kOF^Fz=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@dabh/diagnostics": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", @@ -1684,6 +1708,30 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@types/babel__core": { "version": "7.20.1", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", @@ -1855,10 +1903,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.4.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz", - "integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==", - "dev": true + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/prettier": { "version": "2.7.3", @@ -2146,9 +2197,9 @@ } }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -2166,6 +2217,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2246,6 +2309,12 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2862,6 +2931,12 @@ "node": ">= 0.10" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2971,6 +3046,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/diff-sequences": { "version": "29.4.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", @@ -3907,6 +3991,11 @@ "node": ">= 0.6" } }, + "node_modules/fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7152,6 +7241,49 @@ } } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -7316,9 +7448,9 @@ } }, "node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -7343,6 +7475,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -7403,6 +7541,12 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -7619,6 +7763,15 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index 97ee24fc5..d44476b13 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,7 @@ "format": "prettier '**/*.ts' --write", "build": "tsc", "start": "npx prisma migrate deploy && node dist/src/index.js", - "dev": "NODE_ENV=dev tsx src/index.ts", + "dev": "NODE_ENV=dev & tsx src/index.ts", "dev:watch": "NODE_ENV=dev tsx watch src/index.ts", "test": "jest --coverage --verbose" }, @@ -16,6 +16,7 @@ "cors": "^2.8.5", "envsafe": "^2.0.3", "express": "^4.18.2", + "fs": "^0.0.1-security", "ioredis": "^5.3.2", "jsonwebtoken": "^9.0.1", "node-fetch": "^3.3.2", @@ -30,7 +31,7 @@ "@types/express": "4.17.17", "@types/jest": "29.5.3", "@types/jsonwebtoken": "9.0.2", - "@types/node": "20.4.2", + "@types/node": "^20.14.10", "@types/swagger-ui-express": "^4.1.3", "@typescript-eslint/eslint-plugin": "6.0.0", "@typescript-eslint/parser": "6.0.0", @@ -41,7 +42,8 @@ "prettier": "3.2.5", "prisma": "^5.0.0", "ts-jest": "29.1.1", + "ts-node": "^10.9.2", "tsx": "^3.12.7", - "typescript": "^5.1.6" + "typescript": "^5.5.3" } } diff --git a/backend/prisma/migrations/migration_lock.toml b/backend/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..fbffa92c2 --- /dev/null +++ b/backend/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/backend/prisma/migrations/z0_20241104062037_add_reviews_scraped/migration.sql b/backend/prisma/migrations/z0_20241104062037_add_reviews_scraped/migration.sql new file mode 100644 index 000000000..335573244 --- /dev/null +++ b/backend/prisma/migrations/z0_20241104062037_add_reviews_scraped/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "reviews_scraped" ( + "review_scraped_id" UUID NOT NULL DEFAULT gen_random_uuid(), + "source" TEXT NOT NULL, + "source_id" INTEGER NOT NULL, + "course_code" TEXT NOT NULL, + "author_name" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "term_taken" TEXT NOT NULL, + "created_timestamp" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "upvotes" TEXT[], + "overall_rating" DOUBLE PRECISION NOT NULL, + + CONSTRAINT "pk_review_scraped_id" PRIMARY KEY ("review_scraped_id") +); + +-- AddForeignKey +ALTER TABLE "reviews_scraped" ADD CONSTRAINT "fk_course_code" FOREIGN KEY ("course_code") REFERENCES "courses"("course_code") ON DELETE NO ACTION ON UPDATE NO ACTION; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 623e339d6..8aa64b0bf 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -15,26 +15,27 @@ enum report_status { } model courses { - courseCode String @id(map: "pk_course_code") @map("course_code") - archived Boolean - attributes String[] - calendar String - campus String - description String - enrolmentRules String @map("enrolment_rules") - equivalents String[] - exclusions String[] - faculty String - fieldOfEducation String @map("field_of_education") - genEd Boolean @map("gen_ed") - level Int - school String - studyLevel String @map("study_level") - terms Int[] - title String - uoc Int - rating Float - reviews reviews[] + courseCode String @id(map: "pk_course_code") @map("course_code") + archived Boolean + attributes String[] + calendar String + campus String + description String + enrolmentRules String @map("enrolment_rules") + equivalents String[] + exclusions String[] + faculty String + fieldOfEducation String @map("field_of_education") + genEd Boolean @map("gen_ed") + level Int + school String + studyLevel String @map("study_level") + terms Int[] + title String + uoc Int + rating Float + reviews reviews[] + reviewsScraped reviewsScraped[] } model reports { @@ -70,6 +71,23 @@ model reviews { users users @relation(fields: [zid], references: [zid], onDelete: NoAction, onUpdate: NoAction, map: "fk_zid") } +model reviewsScraped { + reviewId String @id(map: "pk_review_scraped_id") @default(dbgenerated("gen_random_uuid()")) @map("review_scraped_id") @db.Uuid + source String + sourceId Int @map("source_id") + courseCode String @map("course_code") + authorName String @map("author_name") + title String + description String? + termTaken String @map("term_taken") + createdTimestamp DateTime @default(now()) @map("created_timestamp") @db.Timestamp(6) + upvotes String[] + overallRating Float @map("overall_rating") + courses courses @relation(fields: [courseCode], references: [courseCode], onDelete: NoAction, onUpdate: NoAction, map: "fk_course_code") + + @@map("reviews_scraped") +} + model users { zid String @id(map: "pk_zid") bookmarkedReviews String[] @map("bookmarked_reviews") diff --git a/backend/src/api/schemas/review.schema.ts b/backend/src/api/schemas/review.schema.ts index 16723b5d6..9a1ee23dd 100644 --- a/backend/src/api/schemas/review.schema.ts +++ b/backend/src/api/schemas/review.schema.ts @@ -1,3 +1,4 @@ +import { title } from "process"; import { z } from "zod"; const CommonReviewSchema = z @@ -37,6 +38,7 @@ export const BookmarkReviewSchema = z reviewId: z.string(), zid: z.string(), bookmark: z.boolean(), + scraped: z.boolean(), }) .strict(); @@ -47,6 +49,7 @@ export const UpvoteReviewSchema = z reviewId: z.string(), zid: z.string(), upvote: z.boolean(), + scraped: z.boolean(), }) .strict(); @@ -118,3 +121,49 @@ const ReviewsSuccessResponseSchema = z export type ReviewsSuccessResponse = z.infer< typeof ReviewsSuccessResponseSchema >; + +export const ReviewScrapedSchema = z + .object({ + reviewId: z.string(), + source: z.string(), + sourceId: z.number(), + courseCode: z.string(), + authorName: z.string(), + title: z.string(), + description: z.string().nullable(), + termTaken: z.string(), + createdTimestamp: z.date(), + upvotes: z.string().array(), + overallRating: z.number(), + }) + .strict(); + +const ReviewScrapedSuccessResponseSchema = z + .object({ + review: z.array(ReviewScrapedSchema), + }) + .strict(); + +export type ReviewScrapedSuccessResponse = z.infer< + typeof ReviewScrapedSuccessResponseSchema +>; + +const ReviewsScrapedSuccessResponseSchema = z + .object({ + reviews: z.array(ReviewScrapedSchema), + }) + .strict(); + +export type ReviewsScrapedSuccessResponse = z.infer< + typeof ReviewsScrapedSuccessResponseSchema +> + +const AllReviewSuccessResponseSchema = z + .object({ + review: z.union([ReviewSchema, ReviewScrapedSchema]), + }) + .strict() + +export type AllReviewSuccessResponse = z.infer< + typeof AllReviewSuccessResponseSchema +> \ No newline at end of file diff --git a/backend/src/controllers/course.controller.ts b/backend/src/controllers/course.controller.ts index 5582fd2e4..46b48fbed 100644 --- a/backend/src/controllers/course.controller.ts +++ b/backend/src/controllers/course.controller.ts @@ -33,6 +33,24 @@ export class CourseController implements IController { } }, ) + .get( + "/courses/code/all", + async (req: Request, res: Response, next: NextFunction) => { + this.logger.debug(`Received request in GET /courses/code/all`); + try { + const allCourses = await this.courseService.getAllCourseCodes(); + this.logger.info(`Responding to client in GET /courses/code/all`); + return res.status(200).json(allCourses); + } catch (err: any) { + this.logger.warn( + `An error occurred when trying to GET /courses/code/all ${formatError( + err, + )}`, + ); + return next(err); + } + }, + ) .get( "/courses", async (req: Request, res: Response, next: NextFunction) => { diff --git a/backend/src/controllers/review.controller.ts b/backend/src/controllers/review.controller.ts index 648aca8d3..6a53690cf 100644 --- a/backend/src/controllers/review.controller.ts +++ b/backend/src/controllers/review.controller.ts @@ -34,7 +34,8 @@ export class ReviewController implements IController { this.logger.debug(`Received request in /reviews`); try { const result = await this.reviewService.getAllReviews(); - return res.status(200).json(result); + this.logger.info(`Responding to client in GET /reviews`); + return res.status(200).json({ ...result }); } catch (err: any) { this.logger.warn( `An error occurred when trying to GET /reviews ${formatError( @@ -46,7 +47,25 @@ export class ReviewController implements IController { }, ) .get( - "/wrapped/reviews/most-liked", + "/reviews/scraped", + async (req: Request, res: Response, next: NextFunction) => { + this.logger.debug(`Received request in /reviews/scraped`); + try { + const result = await this.reviewService.getAllReviewsScraped(); + this.logger.info(`Responding to client in GET /reviews/scraped`); + return res.status(200).json({ ...result }); + } catch (err: any) { + this.logger.warn( + `An error occurred when trying to GET /reviews/scraped ${formatError( + err, + )}`, + ); + return next(err); + } + }, + ) + .get( + "/wrapped/reviews/most-liked", async (req: Request, res: Response, next: NextFunction) => { this.logger.debug(`Received request in /wrapped/reviews/most-liked`); try { @@ -80,7 +99,7 @@ export class ReviewController implements IController { this.logger.info( `Responding to client in GET /reviews/${courseCode}`, ); - return res.status(200).json(result); + return res.status(200).json({ ...result }); } catch (err: any) { this.logger.warn( `An error occurred when trying to GET /reviews ${formatError( @@ -91,7 +110,62 @@ export class ReviewController implements IController { } }, ) - + .get( + "/reviews/scraped/:courseCode", + async ( + req: Request<{ courseCode: string }, unknown>, + res: Response, + next: NextFunction, + ) => { + this.logger.debug( + `Received request in /reviews/scraped/:courseCode`, + ); + try { + const courseCode: string = req.params.courseCode; + const result = + await this.reviewService.getCourseReviewsScraped(courseCode); + this.logger.info( + `Responding to client in GET /reviews/scraped/${courseCode}`, + ); + return res.status(200).json({ ...result }); + } catch (err: any) { + this.logger.warn( + `An error occurred when trying to GET /reviews/scraped ${formatError( + err, + )}`, + ); + return next(err); + } + }, + ) + .get( + "/reviews/scraped/maxId/:source", + async ( + req: Request<{ source: string }, unknown>, + res: Response, + next: NextFunction, + ) => { + this.logger.debug( + `Received request in /reviews/scraped/maxId/:source`, + ); + try { + const source: string = req.params.source; + const result = + await this.reviewService.getSourceReviewScrapedMaxId(source); + this.logger.info( + `Responding to client in GET /reviews/scraped/maxId/${source}`, + ); + return res.status(200).json({ ...result }); + } catch (err: any) { + this.logger.warn( + `An error occurred when trying to GET /reviews/scraped/maxId ${formatError( + err, + )}`, + ); + return next(err); + } + }, + ) .post( "/reviews", [verifyToken, validationMiddleware(PostReviewSchema, "body")], diff --git a/backend/src/repositories/course.repository.ts b/backend/src/repositories/course.repository.ts index 620ff19a7..2bc576dd6 100644 --- a/backend/src/repositories/course.repository.ts +++ b/backend/src/repositories/course.repository.ts @@ -37,7 +37,13 @@ export class CourseRepository { AVG(r.enjoyability) AS "enjoyability", CAST(COUNT(r.review_id) AS INT) AS "reviewCount" FROM courses c - LEFT JOIN reviews r ON c.course_code = r.course_code + LEFT JOIN + ( + SELECT course_code, review_id, overall_rating, manageability, usefulness, enjoyability FROM unilectives.reviews + UNION ALL + SELECT course_code, review_scraped_id, overall_rating, NULL AS manageability, NULL AS usefulness, NULL AS enjoyability FROM unilectives.reviews_scraped + ) + AS r USING(course_code) GROUP BY c.course_code ORDER BY "reviewCount" DESC `) as any[]; @@ -45,6 +51,20 @@ export class CourseRepository { return courses; } + async getAllCourseCodes(): Promise { + const rawCourses = await this.prisma.courses.findMany({ + select: { + courseCode: true, + }, + }); + + const courses = rawCourses.map((course) => + CourseCodeSchema.parse(course.courseCode), + ); + + return courses; + } + async getCoursesFromOffset(offset: number): Promise { const courses = (await this.prisma.$queryRaw` SELECT @@ -72,7 +92,13 @@ export class CourseRepository { AVG(r.enjoyability) AS "enjoyability", CAST(COUNT(r.review_id) AS INT) AS "reviewCount" FROM courses c - LEFT JOIN reviews r ON c.course_code = r.course_code + LEFT JOIN + ( + SELECT course_code, review_id, overall_rating, manageability, usefulness, enjoyability FROM unilectives.reviews + UNION ALL + SELECT course_code, review_scraped_id, overall_rating, NULL AS manageability, NULL AS usefulness, NULL AS enjoyability FROM unilectives.reviews_scraped + ) + AS r USING(course_code) GROUP BY c.course_code ORDER BY "reviewCount" DESC, c.course_code ASC LIMIT 25 OFFSET ${offset}; @@ -109,7 +135,13 @@ export class CourseRepository { AVG(r.enjoyability) AS "enjoyability", CAST(COUNT(r.review_id) AS INT) AS "reviewCount" FROM courses c - LEFT JOIN reviews r ON c.course_code = r.course_code + LEFT JOIN + ( + SELECT course_code, review_id, overall_rating, manageability, usefulness, enjoyability FROM unilectives.reviews + UNION ALL + SELECT course_code, review_scraped_id, overall_rating, NULL AS manageability, NULL AS usefulness, NULL AS enjoyability FROM unilectives.reviews_scraped + ) + AS r USING(course_code) WHERE c.course_code IN (${courseCodesString}) GROUP BY c.course_code ORDER BY "reviewCount" DESC; @@ -146,7 +178,13 @@ export class CourseRepository { AVG(r.enjoyability) AS "enjoyability", CAST(COUNT(r.review_id) AS INT) AS "reviewCount" FROM courses c - LEFT JOIN reviews r ON c.course_code = r.course_code + LEFT JOIN + ( + SELECT course_code, review_id, overall_rating, manageability, usefulness, enjoyability FROM unilectives.reviews + UNION ALL + SELECT course_code, review_scraped_id, overall_rating, NULL AS manageability, NULL AS usefulness, NULL AS enjoyability FROM unilectives.reviews_scraped + ) + AS r USING(course_code) WHERE c.course_code = '${courseCode}' GROUP BY c.course_code ORDER BY "reviewCount" DESC; @@ -183,7 +221,13 @@ export class CourseRepository { AVG(r.enjoyability) AS "enjoyability", CAST(COUNT(r.review_id) AS INT) AS "reviewCount" FROM courses c - LEFT JOIN reviews r ON c.course_code = r.course_code + LEFT JOIN + ( + SELECT course_code, review_id, overall_rating, manageability, usefulness, enjoyability FROM unilectives.reviews + UNION ALL + SELECT course_code, review_scraped_id, overall_rating, NULL AS manageability, NULL AS usefulness, NULL AS enjoyability FROM unilectives.reviews_scraped + ) + AS r USING(course_code) WHERE c.course_code ILIKE ${searchQuery} OR c.title ILIKE ${searchQuery} GROUP BY c.course_code ORDER BY @@ -263,7 +307,13 @@ export class CourseRepository { AVG(r.enjoyability) AS "enjoyability", CAST(COUNT(r.review_id) AS INT) AS "reviewCount" FROM courses c - LEFT JOIN reviews r ON c.course_code = r.course_code + LEFT JOIN + ( + SELECT course_code, review_id, overall_rating, manageability, usefulness, enjoyability FROM unilectives.reviews + UNION ALL + SELECT course_code, review_scraped_id, overall_rating, NULL AS manageability, NULL AS usefulness, NULL AS enjoyability FROM unilectives.reviews_scraped + ) + AS r USING(course_code) WHERE (c.course_code ILIKE ${searchQuery} OR c.title ILIKE ${searchQuery}) AND c.terms && ${termFilters}::integer[] AND c.faculty ILIKE ANY(${facultyFilters}) @@ -342,7 +392,13 @@ export class CourseRepository { AVG(r.enjoyability) AS "enjoyability", CAST(COUNT(r.review_id) AS INT) AS "reviewCount" FROM courses c - LEFT JOIN reviews r ON c.course_code = r.course_code + LEFT JOIN + ( + SELECT course_code, review_id, overall_rating, manageability, usefulness, enjoyability FROM unilectives.reviews + UNION ALL + SELECT course_code, review_scraped_id, overall_rating, NULL AS manageability, NULL AS usefulness, NULL AS enjoyability FROM unilectives.reviews_scraped + ) + AS r USING(course_code) WHERE (c.course_code ILIKE ${searchQuery} OR c.title ILIKE ${searchQuery}) AND (c.terms = ARRAY[]::integer[] OR c.terms && ${termFilters}::integer[]) AND c.faculty ILIKE ANY(${facultyFilters}) diff --git a/backend/src/repositories/review.repository.ts b/backend/src/repositories/review.repository.ts index d8753543d..d37e5e00e 100644 --- a/backend/src/repositories/review.repository.ts +++ b/backend/src/repositories/review.repository.ts @@ -1,4 +1,4 @@ -import { PrismaClient, reviews } from "@prisma/client"; +import { PrismaClient, reviews, reviewsScraped } from "@prisma/client"; import { PostReviewRequestBody, ReviewSchema, @@ -11,6 +11,20 @@ export class ReviewRepository { return await this.prisma.reviews.findMany(); } + async getAllReviewsScraped(): Promise { + return await this.prisma.reviewsScraped.findMany(); + } + + async getCourseReviewsScraped( + courseCode: string, + ): Promise { + return await this.prisma.reviewsScraped.findMany({ + where: { + courseCode, + }, + }); + } + async getCourseReviews(courseCode: string): Promise { return await this.prisma.reviews.findMany({ where: { @@ -54,6 +68,17 @@ export class ReviewRepository { }); } + async updateScrapedUpvotes(review: { reviewId: string; upvotes: string[] }) { + return await this.prisma.reviewsScraped.update({ + where: { + reviewId: review.reviewId, + }, + data: { + upvotes: review.upvotes, + }, + }); + } + async getReviewsByUser(zid: string): Promise { return await this.prisma.reviews.findMany({ where: { @@ -80,6 +105,25 @@ export class ReviewRepository { }); } + async getReviewScraped(reviewId: string): Promise { + return await this.prisma.reviewsScraped.findUnique({ + where: { + reviewId: reviewId, + }, + }); + } + + async getSourceReviewScrapedMaxId(source: string) { + return await this.prisma.reviewsScraped.aggregate({ + where: { + source: source + }, + _max: { + sourceId: true + } + }) + } + async deleteReview(reviewId: string) { return await this.prisma.reviews.delete({ where: { diff --git a/backend/src/services/course.service.ts b/backend/src/services/course.service.ts index 96c8f8f9a..657e1aead 100644 --- a/backend/src/services/course.service.ts +++ b/backend/src/services/course.service.ts @@ -33,6 +33,17 @@ export class CourseService { return { courses }; } + async getAllCourseCodes(): Promise { + const courseCodes = await this.courseRepository.getAllCourseCodes(); + if (courseCodes.length === 0) { + this.logger.error("Database returned with no course codes."); + throw new HTTPError(internalServerError); + } + + this.logger.info(`Found ${courseCodes.length} course codes.`); + return courseCodes; + } + async getCoursesFromOffset( offset: number, ): Promise { diff --git a/backend/src/services/review.service.test.ts b/backend/src/services/review.service.test.ts index 0375c5a67..f33b8667f 100644 --- a/backend/src/services/review.service.test.ts +++ b/backend/src/services/review.service.test.ts @@ -141,6 +141,7 @@ describe("ReviewService", () => { reviewId: reviews.reviewId, zid: reviews.zid, bookmark: true, + scraped: false }; const errorResult = new HTTPError(badRequest); @@ -158,6 +159,7 @@ describe("ReviewService", () => { reviewId: reviews[0].reviewId, zid: reviews[0].zid, bookmark: true, + scraped: false }; const errorResult = new HTTPError(badRequest); @@ -177,6 +179,7 @@ describe("ReviewService", () => { reviewId: reviews[0].reviewId, zid: reviews[0].zid, bookmark: true, + scraped: false }; expect(service.bookmarkReview(request)).resolves.toEqual({ @@ -197,6 +200,7 @@ describe("ReviewService", () => { reviewId: reviews[0].reviewId, zid: reviews[0].zid, bookmark: false, + scraped: false }; expect(service.bookmarkReview(request)).resolves.toEqual({ review: reviews[0], diff --git a/backend/src/services/review.service.ts b/backend/src/services/review.service.ts index b7aef1b43..f05bf64b9 100644 --- a/backend/src/services/review.service.ts +++ b/backend/src/services/review.service.ts @@ -9,11 +9,13 @@ import { PostReviewRequestBody, PutReviewRequestBody, Review, - ReviewsSuccessResponse, ReviewSuccessResponse, + ReviewsSuccessResponse, + ReviewsScrapedSuccessResponse, + AllReviewSuccessResponse, UpvoteReview, } from "../api/schemas/review.schema"; -import { reviews } from "@prisma/client"; +import { reviews, reviewsScraped } from "@prisma/client"; export class ReviewService { private logger = getLogger(); @@ -34,6 +36,20 @@ export class ReviewService { }; } + async getAllReviewsScraped(): Promise< + ReviewsScrapedSuccessResponse | undefined + > { + const reviews: reviewsScraped[] = + await this.reviewRepository.getAllReviewsScraped(); + if (reviews.length === 0) { + this.logger.error("Database returned with no reviews."); + throw new HTTPError(internalServerError); + } + return { + reviews: reviews, + }; + } + async getCourseReviews( courseCode: string, ): Promise { @@ -62,6 +78,47 @@ export class ReviewService { }; } + async getCourseReviewsScraped( + courseCode: string, + ): Promise { + let reviews = await this.redis.get( + `reviewsScraped:${courseCode}`, + ); + if (!reviews) { + this.logger.info(`Cache miss on reviewsScraped:${courseCode}`); + reviews = + await this.reviewRepository.getCourseReviewsScraped(courseCode); + await this.redis.set(`reviewsScraped:${courseCode}`, reviews); + } else { + this.logger.info(`Cache hit on reviewsScraped:${courseCode}`); + } + + if (reviews.length === 0) { + this.logger.error("Database returned with no reviews."); + throw new HTTPError(internalServerError); + } + this.logger.info(`Found ${reviews.length} reviews.`); + return { + reviews: reviews.map((review) => { + return { + ...review, + courseCode, + }; + }), + }; + } + + async getSourceReviewScrapedMaxId(source: string) { + const res = await this.reviewRepository.getSourceReviewScrapedMaxId(source); + let maxId = 0; + if (res._max && res._max.sourceId) { + maxId = res._max.sourceId; + } + return { + maxId: maxId + } + } + async postReview( reviewDetails: PostReviewRequestBody, ): Promise { @@ -135,10 +192,11 @@ export class ReviewService { async bookmarkReview( reviewDetails: BookmarkReview, - ): Promise { - const review = await this.reviewRepository.getReview( - reviewDetails.reviewId, - ); + ): Promise { + + const review = reviewDetails.scraped ? + await this.reviewRepository.getReviewScraped(reviewDetails.reviewId) : + await this.reviewRepository.getReview(reviewDetails.reviewId) ; if (!review) { this.logger.error( @@ -184,7 +242,9 @@ export class ReviewService { } async upvoteReview(upvoteDetails: UpvoteReview) { - let review = await this.reviewRepository.getReview(upvoteDetails.reviewId); + let review = upvoteDetails.scraped ? + await this.reviewRepository.getReviewScraped(upvoteDetails.reviewId) : + await this.reviewRepository.getReview(upvoteDetails.reviewId) ; if (!review) { this.logger.error( @@ -209,13 +269,19 @@ export class ReviewService { ); } - review = await this.reviewRepository.updateUpvotes(review); + review = upvoteDetails.scraped ? + await this.reviewRepository.updateScrapedUpvotes(review) : + await this.reviewRepository.updateUpvotes(review); - const reviews = await this.reviewRepository.getCourseReviews( - review.courseCode, - ); + const reviews = upvoteDetails.scraped ? + await this.reviewRepository.getCourseReviewsScraped(review.courseCode) : + await this.reviewRepository.getCourseReviews(review.courseCode); - await this.redis.set(`reviews:${review.courseCode}`, reviews); + if (upvoteDetails.scraped) { + await this.redis.set(`reviewsScraped:${review.courseCode}`, reviews); + } else { + await this.redis.set(`reviews:${review.courseCode}`, reviews); + } this.logger.info( `Successfully ${ diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 58ea5e562..a09ba0af0 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -23,11 +23,8 @@ "strict": true /* Enable all strict type-checking options. */, "skipLibCheck": true /* Skip type checking all .d.ts files. */, "strictPropertyInitialization": false, - "resolveJsonModule": true + "resolveJsonModule": true, }, - "include": [ - "src/**/*", - "config/*", - ], + "include": ["src/**/*", "config/*"], "exclude": ["src/**/*.test.ts"] } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index aa9765192..09b64d961 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "@types/node": "18.15.11", "@types/react": "18.0.31", "@types/react-dom": "18.0.11", + "antd": "^5.24.1", "date-fns": "^2.30.0", "lodash": "^4.17.21", "next": "^13.4.4", @@ -47,17 +48,117 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ant-design/colors": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.0.tgz", + "integrity": "sha512-bjTObSnZ9C/O8MB/B4OUtd/q9COomuJAR2SYfhxLyHvCKn4EKwCN3e+fWGMo7H5InAyV0wL17jdE9ALrdOW/6A==", + "dependencies": { + "@ant-design/fast-color": "^2.0.6" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.23.0.tgz", + "integrity": "sha512-7GAg9bD/iC9ikWatU9ym+P9ugJhi/WbsTWzcKN6T4gU0aehsprtke1UAaaSxxkjjmkJb3llet/rbUSLPgwlY4w==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "classnames": "^2.3.1", + "csstype": "^3.1.3", + "rc-util": "^5.35.0", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", + "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==", + "dependencies": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", + "dependencies": { + "@ant-design/colors": "^7.0.0", + "@ant-design/icons-svg": "^4.4.0", + "@babel/runtime": "^7.24.8", + "classnames": "^2.2.6", + "rc-util": "^5.31.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==" + }, + "node_modules/@ant-design/react-slick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", + "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", + "dependencies": { + "@babel/runtime": "^7.10.4", + "classnames": "^2.2.5", + "json2mq": "^0.2.0", + "resize-observer-polyfill": "^1.5.1", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": ">=16.9.0" + } + }, "node_modules/@babel/runtime": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", - "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", + "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "node_modules/@eslint-community/regexpp": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", @@ -512,6 +613,146 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@rc-component/async-validator": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", + "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", + "dependencies": { + "@ant-design/fast-color": "^2.0.6", + "@babel/runtime": "^7.23.6", + "classnames": "^2.2.6", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", + "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", + "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", + "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", + "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.0.0.tgz", + "integrity": "sha512-L+rZ4HXP2sJ1gHMGHjsg9jlYBX/SLN2D6OxP9Zn3qgtpMWtO2vUfxVFwiogHpAIqs54FnALxraUy/BCO1yRIgg==", + "dependencies": { + "@babel/runtime": "^7.24.7", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/portal": "^1.0.0-9", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.2", + "rc-util": "^5.24.4" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/trigger": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.6.tgz", + "integrity": "sha512-/9zuTnWwhQ3S3WT1T8BubuFTT46kvnXgaERR9f4BTKyn61/wpf/BvbImzYBubzJibU707FxwbKszLlHjcLiv1Q==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@rc-component/portal": "^1.1.0", + "classnames": "^2.3.2", + "rc-motion": "^2.0.0", + "rc-resize-observer": "^1.3.1", + "rc-util": "^5.44.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/@rushstack/eslint-patch": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.2.tgz", @@ -733,6 +974,70 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/antd": { + "version": "5.24.1", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.24.1.tgz", + "integrity": "sha512-RGwpXpSr2RtoUnrpJl3V6ZaTExwSXkFVxV24VUowwC04n6oA1sGyJrofQOKNqD623sVxL5UJBmf0a+BFBImP3Q==", + "dependencies": { + "@ant-design/colors": "^7.2.0", + "@ant-design/cssinjs": "^1.23.0", + "@ant-design/cssinjs-utils": "^1.1.3", + "@ant-design/fast-color": "^2.0.6", + "@ant-design/icons": "^5.6.1", + "@ant-design/react-slick": "~1.1.2", + "@babel/runtime": "^7.26.0", + "@rc-component/color-picker": "~2.0.1", + "@rc-component/mutate-observer": "^1.1.0", + "@rc-component/qrcode": "~1.0.0", + "@rc-component/tour": "~1.15.1", + "@rc-component/trigger": "^2.2.6", + "classnames": "^2.5.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.11", + "rc-cascader": "~3.33.0", + "rc-checkbox": "~3.5.0", + "rc-collapse": "~3.9.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.2.0", + "rc-dropdown": "~4.2.1", + "rc-field-form": "~2.7.0", + "rc-image": "~7.11.0", + "rc-input": "~1.7.2", + "rc-input-number": "~9.4.0", + "rc-mentions": "~2.19.1", + "rc-menu": "~9.16.0", + "rc-motion": "^2.9.5", + "rc-notification": "~5.6.3", + "rc-pagination": "~5.1.0", + "rc-picker": "~4.11.1", + "rc-progress": "~4.0.0", + "rc-rate": "~2.13.1", + "rc-resize-observer": "^1.4.3", + "rc-segmented": "~2.7.0", + "rc-select": "~14.16.6", + "rc-slider": "~11.1.8", + "rc-steps": "~6.0.1", + "rc-switch": "~4.1.0", + "rc-table": "~7.50.3", + "rc-tabs": "~15.5.1", + "rc-textarea": "~1.9.0", + "rc-tooltip": "~6.4.0", + "rc-tree": "~5.13.0", + "rc-tree-select": "~5.27.0", + "rc-upload": "~4.8.1", + "rc-util": "^5.44.4", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1230,6 +1535,11 @@ "node": ">= 6" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -1261,6 +1571,11 @@ "node": ">= 6" } }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1274,6 +1589,14 @@ "node": ">= 0.6" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1300,9 +1623,9 @@ } }, "node_modules/csstype": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", - "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -1376,6 +1699,11 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3380,6 +3708,14 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "dependencies": { + "string-convert": "^0.2.0" + } + }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -4340,6 +4676,583 @@ } ] }, + "node_modules/rc-cascader": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.33.0.tgz", + "integrity": "sha512-JvZrMbKBXIbEDmpIORxqvedY/bck6hGbs3hxdWT8eS9wSQ1P7//lGxbyKjOSyQiVBbgzNWriSe6HoMcZO/+0rQ==", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "^2.3.1", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-checkbox": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", + "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.25.2" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-collapse": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz", + "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.3.4", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dialog": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/portal": "^1.0.0-8", + "classnames": "^2.2.6", + "rc-motion": "^2.3.0", + "rc-util": "^5.21.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-drawer": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.2.0.tgz", + "integrity": "sha512-9lOQ7kBekEJRdEpScHvtmEtXnAsy+NGDXiRWc2ZVC7QXAazNVbeT4EraQKYwCME8BJLa8Bxqxvs5swwyOepRwg==", + "dependencies": { + "@babel/runtime": "^7.23.9", + "@rc-component/portal": "^1.1.1", + "classnames": "^2.2.6", + "rc-motion": "^2.6.1", + "rc-util": "^5.38.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-dropdown": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz", + "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-util": "^5.44.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/rc-field-form": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.0.tgz", + "integrity": "sha512-hgKsCay2taxzVnBPZl+1n4ZondsV78G++XVsMIJCAoioMjlMQR9YwAp7JZDIECzIu2Z66R+f4SFIRrO2DjDNAA==", + "dependencies": { + "@babel/runtime": "^7.18.0", + "@rc-component/async-validator": "^5.0.3", + "rc-util": "^5.32.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-image": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.11.0.tgz", + "integrity": "sha512-aZkTEZXqeqfPZtnSdNUnKQA0N/3MbgR7nUnZ+/4MfSFWPFHZau4p5r5ShaI0KPEMnNjv4kijSCFq/9wtJpwykw==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/portal": "^1.0.2", + "classnames": "^2.2.6", + "rc-dialog": "~9.6.0", + "rc-motion": "^2.6.2", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-input": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.7.2.tgz", + "integrity": "sha512-g3nYONnl4edWj2FfVoxsU3Ec4XTE+Hb39Kfh2MFxMZjp/0gGyPUgy/v7ZhS27ZxUFNkuIDYXm9PJsLyJbtg86A==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.18.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-input-number": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.4.0.tgz", + "integrity": "sha512-Tiy4DcXcFXAf9wDhN8aUAyMeCLHJUHA/VA/t7Hj8ZEx5ETvxG7MArDOSE6psbiSCo+vJPm4E3fGN710ITVn6GA==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/mini-decimal": "^1.0.1", + "classnames": "^2.2.5", + "rc-input": "~1.7.1", + "rc-util": "^5.40.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-mentions": { + "version": "2.19.1", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.19.1.tgz", + "integrity": "sha512-KK3bAc/bPFI993J3necmaMXD2reZTzytZdlTvkeBbp50IGH1BDPDvxLdHDUrpQx2b2TGaVJsn+86BvYa03kGqA==", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.6", + "rc-input": "~1.7.1", + "rc-menu": "~9.16.0", + "rc-textarea": "~1.9.0", + "rc-util": "^5.34.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-menu": { + "version": "9.16.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.0.0", + "classnames": "2.x", + "rc-motion": "^2.4.3", + "rc-overflow": "^1.3.1", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-motion": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", + "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-util": "^5.44.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-notification": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.3.tgz", + "integrity": "sha512-42szwnn8VYQoT6GnjO00i1iwqV9D1TTMvxObWsuLwgl0TsOokzhkYiufdtQBsJMFjJravS1hfDKVMHLKLcPE4g==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.9.0", + "rc-util": "^5.20.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-overflow": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.4.1.tgz", + "integrity": "sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.37.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-pagination": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz", + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-picker": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.2.tgz", + "integrity": "sha512-Cwa3frWpefhESBF20HBJtvWx3q1hCrMxSUrzuuWMTGoZVPhQllGEp2IUfzo9jC5LKm4kJx7IrH8q/W/y9wClAw==", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.2.1", + "rc-overflow": "^1.3.2", + "rc-resize-observer": "^1.4.0", + "rc-util": "^5.43.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/rc-progress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", + "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.6", + "rc-util": "^5.16.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-rate": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.0.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-resize-observer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", + "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", + "dependencies": { + "@babel/runtime": "^7.20.7", + "classnames": "^2.2.1", + "rc-util": "^5.44.1", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-segmented": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.0.tgz", + "integrity": "sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "classnames": "^2.2.1", + "rc-motion": "^2.4.4", + "rc-util": "^5.17.0" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/rc-select": { + "version": "14.16.6", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.6.tgz", + "integrity": "sha512-YPMtRPqfZWOm2XGTbx5/YVr1HT0vn//8QS77At0Gjb3Lv+Lbut0IORJPKLWu1hQ3u4GsA0SrDzs7nI8JG7Zmyg==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/trigger": "^2.1.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-overflow": "^1.3.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-slider": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.8.tgz", + "integrity": "sha512-2gg/72YFSpKP+Ja5AjC5DPL1YnV8DEITDQrcc1eASrUYjl0esptaBVJBh5nLTXCCp15eD8EuGjwezVGSHhs9tQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.5", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-steps": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", + "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "dependencies": { + "@babel/runtime": "^7.16.7", + "classnames": "^2.2.3", + "rc-util": "^5.16.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-switch": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", + "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "dependencies": { + "@babel/runtime": "^7.21.0", + "classnames": "^2.2.1", + "rc-util": "^5.30.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-table": { + "version": "7.50.3", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.3.tgz", + "integrity": "sha512-Z4/zNCzjv7f/XzPRecb+vJU0DJKdsYt4YRkDzNl4G05m7JmxrKGYC2KqN1Ew6jw2zJq7cxVv3z39qyZOHMuf7A==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/context": "^1.4.0", + "classnames": "^2.2.5", + "rc-resize-observer": "^1.1.0", + "rc-util": "^5.44.3", + "rc-virtual-list": "^3.14.2" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tabs": { + "version": "15.5.1", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.5.1.tgz", + "integrity": "sha512-yiWivLAjEo5d1v2xlseB2dQocsOhkoVSfo1krS8v8r+02K+TBUjSjXIf7dgyVSxp6wRIPv5pMi5hanNUlQMgUA==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "classnames": "2.x", + "rc-dropdown": "~4.2.0", + "rc-menu": "~9.16.0", + "rc-motion": "^2.6.2", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.34.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-textarea": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.9.0.tgz", + "integrity": "sha512-dQW/Bc/MriPBTugj2Kx9PMS5eXCCGn2cxoIaichjbNvOiARlaHdI99j4DTxLl/V8+PIfW06uFy7kjfUIDDKyxQ==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "^2.2.1", + "rc-input": "~1.7.1", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.27.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tooltip": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", + "dependencies": { + "@babel/runtime": "^7.11.2", + "@rc-component/trigger": "^2.0.0", + "classnames": "^2.3.1", + "rc-util": "^5.44.3" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-tree": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.0.tgz", + "integrity": "sha512-2+lFvoVRnvHQ1trlpXMOWtF8BUgF+3TiipG72uOfhpL5CUdXCk931kvDdUkTL/IZVtNEDQKwEEmJbAYJSA5NnA==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "classnames": "2.x", + "rc-motion": "^2.0.1", + "rc-util": "^5.16.1", + "rc-virtual-list": "^3.5.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-tree-select": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", + "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", + "dependencies": { + "@babel/runtime": "^7.25.7", + "classnames": "2.x", + "rc-select": "~14.16.2", + "rc-tree": "~5.13.0", + "rc-util": "^5.43.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/rc-upload": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.8.1.tgz", + "integrity": "sha512-toEAhwl4hjLAI1u8/CgKWt30BR06ulPa4iGQSMvSXoHzO88gPCslxqV/mnn4gJU7PDoltGIC9Eh+wkeudqgHyw==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "classnames": "^2.2.5", + "rc-util": "^5.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util": { + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", + "dependencies": { + "@babel/runtime": "^7.18.3", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/rc-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/rc-virtual-list": { + "version": "3.18.2", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.18.2.tgz", + "integrity": "sha512-SkPabqstOQgJ2Q2Ob3eDPIHsNrDzQZFl8mzHiXuNablyYwddVU33Ws6oxoA7Fi/6pZeEYonrLEUiJGr/6aBVaw==", + "dependencies": { + "@babel/runtime": "^7.20.0", + "classnames": "^2.2.6", + "rc-resize-observer": "^1.0.0", + "rc-util": "^5.36.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -4423,9 +5336,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", @@ -4445,6 +5358,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -4680,6 +5598,14 @@ "typescript": ">=4.1.0" } }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, "node_modules/semver": { "version": "7.5.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", @@ -4809,6 +5735,11 @@ "node": ">=10.0.0" } }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==" + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -5029,6 +5960,11 @@ } } }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==" + }, "node_modules/sucrase": { "version": "3.32.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", @@ -5218,6 +6154,14 @@ "node": ">=0.8" } }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "engines": { + "node": ">=12.22" + } + }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -5241,6 +6185,11 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", diff --git a/frontend/package.json b/frontend/package.json index 48b0bef7d..cdf0a3aab 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "@types/node": "18.15.11", "@types/react": "18.0.31", "@types/react-dom": "18.0.11", + "antd": "^5.24.1", "date-fns": "^2.30.0", "lodash": "^4.17.21", "next": "^13.4.4", diff --git a/frontend/src/app/course/[id]/page.tsx b/frontend/src/app/course/[id]/page.tsx index 5f9e3d371..d9281afa9 100644 --- a/frontend/src/app/course/[id]/page.tsx +++ b/frontend/src/app/course/[id]/page.tsx @@ -4,7 +4,7 @@ import ReviewSearchbar from "@/components/ReviewSearchBar/ReviewSearchBar"; import ReviewsBar from "@/components/ReviewsBar/ReviewsBar"; import TermsGroup from "@/components/TermsGroup/TermsGroup"; import { authOptions } from "@/lib/auth"; -import { Course, Reviews } from "@/types/api"; +import { Course, Reviews, Review } from "@/types/api"; import { get, validatedReq } from "@/utils/request"; import { LinkIcon } from "@heroicons/react/24/solid"; import { Metadata } from "next"; @@ -53,6 +53,15 @@ export default async function ReviewPage({ `/reviews/${course.courseCode.toUpperCase()}`, )) as Reviews; + const { reviews: reviewsScraped } = (await get( + `/reviews/scraped/${course.courseCode.toUpperCase()}` + )) as Reviews; + + const allReviews: Review[] = [ + ...(reviews || []), + ...(reviewsScraped || []) + ]; + let userCourseInfo: string[] = []; if (session?.user) { try { @@ -221,7 +230,7 @@ export default async function ReviewPage({ Loading...}> diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index fb459b892..f3c99e031 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -7,6 +7,7 @@ import { ItemList, WithContext } from "schema-dts"; import { get } from "@/utils/request"; import { Course, Courses } from "@/types/api"; + export async function generateMetadata(): Promise { return { title: `Home | Unilectives - UNSW Course Reviews`, diff --git a/frontend/src/components/EditReviewModal/EditReviewModal.tsx b/frontend/src/components/EditReviewModal/EditReviewModal.tsx index 244d3b341..c24b468cd 100644 --- a/frontend/src/components/EditReviewModal/EditReviewModal.tsx +++ b/frontend/src/components/EditReviewModal/EditReviewModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { Review } from "@/types/api"; +import { ReviewNative } from "@/types/api"; import { Dialog, Transition } from "@headlessui/react"; import { PencilSquareIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { FormEvent, Fragment, useState } from "react"; @@ -14,7 +14,7 @@ export default function EditReviewModal({ review, setEdited, }: { - review: Review; + review: ReviewNative; setEdited: (detail: { reviewId: string; authorName: string; diff --git a/frontend/src/components/FilterModal/FilterModal.tsx b/frontend/src/components/FilterModal/FilterModal.tsx index ce40b02b2..11b31b7ec 100644 --- a/frontend/src/components/FilterModal/FilterModal.tsx +++ b/frontend/src/components/FilterModal/FilterModal.tsx @@ -105,15 +105,6 @@ export default function FilterModal({ } }); - // Show secret message if specific filters are selected - if ( - selectedFaculties.includes("Arts") && - selectedTerms.includes("-1") && - selectedTerms.includes("-2") - ) { - alert("Alss aol zlhyjo ihy..."); - } - setFilters({ faculties: selectedFaculties, terms: selectedTerms }); setOpen(false); diff --git a/frontend/src/components/ReviewCard/ReviewCard.tsx b/frontend/src/components/ReviewCard/ReviewCard.tsx index 8cd966f7d..991264c31 100644 --- a/frontend/src/components/ReviewCard/ReviewCard.tsx +++ b/frontend/src/components/ReviewCard/ReviewCard.tsx @@ -36,6 +36,7 @@ export default function ReviewCard({ reviewId: review.reviewId, zid: session?.user?.id, upvote, + scraped: !('enjoyability' in review), // no scraped review has enjoyability }; await validatedReq( "POST", @@ -65,13 +66,14 @@ export default function ReviewCard({ reviewId: review.reviewId, zid: session?.user?.id, bookmark: !bookmarked, + scraped: !('enjoyability' in review), // no scraped review has enjoyability }; await validatedReq( "POST", "/reviews/bookmark", session?.user?.accessToken ?? "", session?.user?.id ?? "", - body + body, ); // Optimistic UI update for bookmark setAllBookmarkedReviews((prev: string[]) => { @@ -109,12 +111,15 @@ export default function ReviewCard({ {/* Term taken + Grade */}

Term taken: {review.termTaken}

-

- Grade: {!review.grade ? "-" : review.grade} -

+ {'grade' in review && ( +

+ Grade: {!review.grade ? "-" : review.grade} +

+ )}
{/* Circle rating */} -
+ {'enjoyability' in review && ( +
{/* Enjoyability */}

Enjoyment

@@ -143,6 +148,7 @@ export default function ReviewCard({ />
+ )} {/* Description */} {/* Icons */} @@ -151,12 +157,10 @@ export default function ReviewCard({
); } diff --git a/frontend/src/components/UserPageContent/UserPageContent.tsx b/frontend/src/components/UserPageContent/UserPageContent.tsx index 84ed9b333..723682376 100644 --- a/frontend/src/components/UserPageContent/UserPageContent.tsx +++ b/frontend/src/components/UserPageContent/UserPageContent.tsx @@ -1,7 +1,7 @@ "use client"; -import { Dispatch, SetStateAction, useCallback, useRef, useState } from "react"; -import { Course, Report, Review, TabsType } from "@/types/api"; +import { useCallback, useRef, useState } from "react"; +import { Course, Report, Review, ReviewNative, TabsType } from "@/types/api"; import UserReviews from "../UserReviews/UserReviews"; import UserReports from "../UserReports/UserReports"; import Dropdown from "../Dropdown/Dropdown"; @@ -77,7 +77,7 @@ export default function UserPageContent({ {/* My reviews */} {tabs["My reviews"].current && ( )} diff --git a/frontend/src/components/UserReports/UserReports.tsx b/frontend/src/components/UserReports/UserReports.tsx index 6141e827f..298c33737 100644 --- a/frontend/src/components/UserReports/UserReports.tsx +++ b/frontend/src/components/UserReports/UserReports.tsx @@ -10,7 +10,6 @@ import { } from "react"; import Dropdown from "../Dropdown/Dropdown"; import Pagination from "../Pagination/Pagination"; -import ReviewCard from "../ReviewCard/ReviewCard"; type STATUS = { UNSEEN: boolean; @@ -183,15 +182,11 @@ export default function UserReports({ ))} {/* Pagination */} - {reports.length > 0 ? ( - setPage(page)} - /> - ) : ( -
No reports made yet.
- )} + setPage(page)} + /> ); } diff --git a/frontend/src/components/UserReviews/UserReviews.tsx b/frontend/src/components/UserReviews/UserReviews.tsx index 9638d3193..7074ade1f 100644 --- a/frontend/src/components/UserReviews/UserReviews.tsx +++ b/frontend/src/components/UserReviews/UserReviews.tsx @@ -1,6 +1,6 @@ "use client"; -import { Course, Review, Reviews, TabsType } from "@/types/api"; +import { ReviewNative, TabsType, ReviewsNative } from "@/types/api"; import Dropdown from "../Dropdown/Dropdown"; import { Dispatch, SetStateAction, useEffect, useState } from "react"; import Rating from "../Rating/Rating"; @@ -12,7 +12,7 @@ import RemoveReviewModal from "../RemoveReviewModal/RemoveReviewModal"; export default function UserReviews({ reviews, setTabs, -}: Reviews & { +}: ReviewsNative & { setTabs: Dispatch>; }) { const [selected, setSelected] = useState(""); @@ -32,23 +32,23 @@ export default function UserReviews({ switch (selected) { case "Most Recent": sortedReviews.sort( - (r1: Review, r2: Review) => + (r1: ReviewNative, r2: ReviewNative) => Date.parse(r2.createdTimestamp) - Date.parse(r1.createdTimestamp) ); break; case "Most Recently Taken": - sortedReviews.sort((r1: Review, r2: Review) => + sortedReviews.sort((r1: ReviewNative, r2: ReviewNative) => r2.termTaken.localeCompare(r1.termTaken) ); break; case "Highest Rating to Lowest Rating": sortedReviews.sort( - (r1: Review, r2: Review) => r2.overallRating - r1.overallRating + (r1: ReviewNative, r2: ReviewNative) => r2.overallRating - r1.overallRating ); break; case "Lowest Rating to Highest Rating": sortedReviews.sort( - (r1: Review, r2: Review) => r1.overallRating - r2.overallRating + (r1: ReviewNative, r2: ReviewNative) => r1.overallRating - r2.overallRating ); break; } @@ -123,7 +123,7 @@ export default function UserReviews({
{reviews .slice((page - 1) * itemPerPage, page * itemPerPage) - .map((review: Review, index: number) => ( + .map((review: ReviewNative, index: number) => (
{reviews .slice((page - 1) * itemPerPage, page * itemPerPage) - .map((review: Review, index: number) => ( + .map((review: ReviewNative, index: number) => (
)} {/* Pagination */} - {reviews.length > 0 ? ( - setPage(page)} - /> - ) : ( -
No courses reviewed yet.
- )} + setPage(page)} + />
); } diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index d37dbeea5..691e38bf9 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -3,7 +3,9 @@ export type ApiError = { errorMessage: string; }; -export type Review = { +export type Review = ReviewNative | ReviewScraped; + +export type ReviewNative = { reviewId: string; courseCode: string; authorName: string; @@ -20,10 +22,26 @@ export type Review = { overallRating: number; }; +export type ReviewScraped = { + reviewId: string; + courseCode: string; + authorName: string; + title: string; + description: string; + termTaken: string; + createdTimestamp: string; + upvotes: string[]; + overallRating: number; +}; + export type Reviews = { reviews: Review[]; }; +export type ReviewsNative = { + reviews: ReviewNative[]; +}; + export type ReportStatus = "UNSEEN" | "SEEN" | "REMOVED" | "SETTLED"; export type Report = { diff --git a/scraper/.gitignore b/scraper/.gitignore new file mode 100644 index 000000000..4f34dec4a --- /dev/null +++ b/scraper/.gitignore @@ -0,0 +1,2 @@ +studentVIP_reviews.json +uninotes_reviews.json \ No newline at end of file diff --git a/scraper/studentVIP.py b/scraper/studentVIP.py new file mode 100644 index 000000000..4f102e511 --- /dev/null +++ b/scraper/studentVIP.py @@ -0,0 +1,56 @@ +from bs4 import BeautifulSoup +import requests + +website = "StudentVIP" + +# Get all course codes from localhost:3030/api/v1/courses/code/all +url = "http://localhost:3030/api/v1/courses/code/all" +response = requests.get(url) +courses = response.json() + +# Get latest studentvip review ID +start_id = requests.get("http://localhost:3030/api/v1/reviews/scraped/maxId/StudentVIP").json()['maxId'] + +course_codes = courses +url_prefix = "https://studentvip.com.au/unsw/subjects/" +review_values = [] +batch_size = 1000 +count = 0 + +for course_code in course_codes: + page = requests.get(url_prefix + course_code) + soup = BeautifulSoup(page.content, "html.parser") + + try: + res = soup.find("h3", class_="text-subjects") \ + .find_next(class_="list-group") \ + .find_all(class_="panel-body") + + for review in res: + rating = len(review.find_all("i", class_="fa fa-star")) + description = review.find("p").get_text(strip=True).replace("'", "''") # Escape single quotes + name, term, year = review.find("small").get_text(strip=True).split(",") + author_name = name.strip().replace("'", "''") # Escape single quotes + term_taken = term.split(' ') + year_taken = year.strip() + term_taken = year_taken[2:] + term_taken[1][0] + term_taken[2] + + print("Scraped review for course ", course_code) + count += 1 + cur_id = start_id + count + review_values.append( + f"('{course_code}', '{website}', '{cur_id}', 'Review #{cur_id}', {rating}, '{description}', '{author_name}', '{term_taken}', '{{}}')" + ) + + except Exception as e: + print(f"Could not process reviews for {course_code}: {str(e)}") + +# Write SQL statements to a file +with open('../backend/data/studentVIP_reviews.sql', 'w') as f: + f.write("-- StudentVIP Reviews SQL Import\n\n") + for i in range(0, len(review_values), batch_size): + batch = review_values[i:i+batch_size] + sql = f"INSERT INTO unilectives.reviews_scraped (course_code, source, source_id, title, overall_rating, description, author_name, term_taken, upvotes) VALUES {', '.join(batch)};\n" + f.write(sql) + +print(f"SQL file created with {len(review_values)} INSERT statements.") diff --git a/scraper/uniNotes.py b/scraper/uniNotes.py new file mode 100644 index 000000000..31be9159b --- /dev/null +++ b/scraper/uniNotes.py @@ -0,0 +1,77 @@ +from bs4 import BeautifulSoup +import re +import requests +import signal +import time + +website = "UniNotes" + +# Get all course codes from localhost:3030/api/v1/courses/code/all +url = "http://localhost:3030/api/v1/courses/code/all" +response = requests.get(url) +courses = response.json() + +# Get latest studentvip review ID +start_id = requests.get(f"http://localhost:3030/api/v1/reviews/scraped/maxId/{website}").json()['maxId'] +course_codes = courses +url_prefix = "https://uninotes.com/university-subjects/university-of-new-south-wales-unsw/" +review_values = [] +count = 0 + +# Define a timeout handler +def handler(signum, frame): + raise TimeoutError("Loop iteration timed out") + +# Set the timeout duration (in seconds) in case the scraping takes too long +timeout_duration = 10 + +for course_code in course_codes: + signal.signal(signal.SIGALRM, handler) + signal.alarm(timeout_duration) + + try: + page = requests.get(url_prefix + course_code) + soup = BeautifulSoup(page.content, "html.parser") + res = soup.find_all(attrs={"id": re.compile(r'^review')}) + for review in res: + author_name = review.find("h2").get_text(strip=True).replace("'", "''") + + unformattedDesc = '\n'.join(review.find(class_="details").stripped_strings) + + description = re.search(r"Comments\n(.+?)\nContact Hours", unformattedDesc, re.DOTALL) + description = description.group(1).strip().replace("\n", " ").replace("'", "''") if description else "" + + rating = re.search(r"Overall Rating\n([0-9.]+)/5", unformattedDesc) + rating = float(rating.group(1)) if rating else 0 + + grade = re.search(r"Your Mark / Grade\n([\dA-Z ]+)", unformattedDesc) + grade = grade.group(1).strip().replace("'", "''") if grade else "Unknown" + + term_taken_match = re.search(r'Year & (Trimester|Semester) Of Completion\n(.+?)\nYour Mark / Grade', unformattedDesc) + term_taken = term_taken_match.group(2).replace("'", "''") if term_taken_match else "Unknown" + + count += 1 + cur_id = start_id + count + review_values.append( + f"('{course_code}', '{website}', '{cur_id}', 'Review #{cur_id}', {rating}, '{description}', '{author_name}', '{term_taken}', '{{}}')" + ) + if len(res) > 0: + print(f"Scraped reviews for {course_code}...") + except TimeoutError: + print(f"Iteration timed out, moving to next iteration.") + except Exception as e: + print(f"Could not process reviews for {course_code}: {str(e)}") + finally: + signal.alarm(0) # Disable the alarm after each iteration + +# Write SQL statements to a file +with open('../backend/data/uninotes_reviews.sql', 'w') as f: + f.write("-- UniNotes Reviews SQL Import\n\n") + + batch_size = 1000 + for i in range(0, len(review_values), batch_size): + batch = review_values[i:i+batch_size] + sql = f"INSERT INTO unilectives.reviews_scraped (course_code, source, source_id, title, overall_rating, description, author_name, term_taken, upvotes) VALUES {', '.join(batch)};\n" + f.write(sql) + +print(f"SQL file created with {len(review_values)} reviews in multi-insert statements.") \ No newline at end of file From b63430bd7ce0fa3c5b0d1fc3115cb4952fff0bff Mon Sep 17 00:00:00 2001 From: Alec Date: Wed, 18 Jun 2025 10:20:10 +1000 Subject: [PATCH 2/3] Clean reset: make develop code match main exactly --- .DS_Store | Bin 6148 -> 0 bytes backend/.gitignore | 3 +- backend/package-lock.json | 177 +--- backend/package.json | 8 +- backend/prisma/migrations/migration_lock.toml | 3 - .../migration.sql | 19 - backend/prisma/schema.prisma | 58 +- backend/src/api/schemas/review.schema.ts | 49 - backend/src/controllers/course.controller.ts | 18 - backend/src/controllers/review.controller.ts | 82 +- backend/src/repositories/course.repository.ts | 70 +- backend/src/repositories/review.repository.ts | 46 +- backend/src/services/course.service.ts | 11 - backend/src/services/review.service.test.ts | 4 - backend/src/services/review.service.ts | 90 +- backend/tsconfig.json | 7 +- frontend/package-lock.json | 969 +----------------- frontend/package.json | 1 - frontend/src/app/course/[id]/page.tsx | 13 +- frontend/src/app/page.tsx | 1 - .../EditReviewModal/EditReviewModal.tsx | 4 +- .../components/FilterModal/FilterModal.tsx | 9 + .../src/components/ReviewCard/ReviewCard.tsx | 26 +- .../components/ReviewModal/ReviewModal.tsx | 8 + .../src/components/SearchBar/SearchBar.tsx | 4 + .../SubcomRecruitmentPopup.tsx | 31 - .../TruncatedDescription.tsx | 10 +- .../UserBookmarkedReviews.tsx | 14 +- .../UserPageContent/UserPageContent.tsx | 6 +- .../components/UserReports/UserReports.tsx | 15 +- .../components/UserReviews/UserReviews.tsx | 30 +- frontend/src/types/api.ts | 20 +- scraper/.gitignore | 2 - scraper/studentVIP.py | 56 - scraper/uniNotes.py | 77 -- 35 files changed, 156 insertions(+), 1785 deletions(-) delete mode 100644 .DS_Store delete mode 100644 backend/prisma/migrations/migration_lock.toml delete mode 100644 backend/prisma/migrations/z0_20241104062037_add_reviews_scraped/migration.sql delete mode 100644 frontend/src/components/SubcomRecruitmentPopup/SubcomRecruitmentPopup.tsx delete mode 100644 scraper/.gitignore delete mode 100644 scraper/studentVIP.py delete mode 100644 scraper/uniNotes.py diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 93a2aa01fc027d1924d61ff67fcf1f78e940d5ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKJ8nWj3>+s&K}thOxmVx@D@0C^3nT$LBoHY5t8y-mmhneX(1R*UgT|6QyMCUx z+9{r&0od|$vjAoQrgTSqc^I2NcOTh9WsFGYI}X_5xH+wNANx`D^@MYuaKai#y#MC! zch?EZN&zV#1*Cu!kOF^Fz=0.1.90" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "node_modules/@dabh/diagnostics": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", @@ -1708,30 +1684,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true - }, "node_modules/@types/babel__core": { "version": "7.20.1", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", @@ -1903,13 +1855,10 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", - "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } + "version": "20.4.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.2.tgz", + "integrity": "sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==", + "dev": true }, "node_modules/@types/prettier": { "version": "2.7.3", @@ -2197,9 +2146,9 @@ } }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -2217,18 +2166,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.3", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", - "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", - "dev": true, - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2309,12 +2246,6 @@ "node": ">= 8" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2931,12 +2862,6 @@ "node": ">= 0.10" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3046,15 +2971,6 @@ "node": ">=8" } }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/diff-sequences": { "version": "29.4.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.4.3.tgz", @@ -3991,11 +3907,6 @@ "node": ">= 0.6" } }, - "node_modules/fs": { - "version": "0.0.1-security", - "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", - "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7241,49 +7152,6 @@ } } }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -7448,9 +7316,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -7475,12 +7343,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -7541,12 +7403,6 @@ "node": ">= 0.4.0" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -7763,15 +7619,6 @@ "node": ">=12" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index d44476b13..97ee24fc5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,7 @@ "format": "prettier '**/*.ts' --write", "build": "tsc", "start": "npx prisma migrate deploy && node dist/src/index.js", - "dev": "NODE_ENV=dev & tsx src/index.ts", + "dev": "NODE_ENV=dev tsx src/index.ts", "dev:watch": "NODE_ENV=dev tsx watch src/index.ts", "test": "jest --coverage --verbose" }, @@ -16,7 +16,6 @@ "cors": "^2.8.5", "envsafe": "^2.0.3", "express": "^4.18.2", - "fs": "^0.0.1-security", "ioredis": "^5.3.2", "jsonwebtoken": "^9.0.1", "node-fetch": "^3.3.2", @@ -31,7 +30,7 @@ "@types/express": "4.17.17", "@types/jest": "29.5.3", "@types/jsonwebtoken": "9.0.2", - "@types/node": "^20.14.10", + "@types/node": "20.4.2", "@types/swagger-ui-express": "^4.1.3", "@typescript-eslint/eslint-plugin": "6.0.0", "@typescript-eslint/parser": "6.0.0", @@ -42,8 +41,7 @@ "prettier": "3.2.5", "prisma": "^5.0.0", "ts-jest": "29.1.1", - "ts-node": "^10.9.2", "tsx": "^3.12.7", - "typescript": "^5.5.3" + "typescript": "^5.1.6" } } diff --git a/backend/prisma/migrations/migration_lock.toml b/backend/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92c2..000000000 --- a/backend/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file diff --git a/backend/prisma/migrations/z0_20241104062037_add_reviews_scraped/migration.sql b/backend/prisma/migrations/z0_20241104062037_add_reviews_scraped/migration.sql deleted file mode 100644 index 335573244..000000000 --- a/backend/prisma/migrations/z0_20241104062037_add_reviews_scraped/migration.sql +++ /dev/null @@ -1,19 +0,0 @@ --- CreateTable -CREATE TABLE "reviews_scraped" ( - "review_scraped_id" UUID NOT NULL DEFAULT gen_random_uuid(), - "source" TEXT NOT NULL, - "source_id" INTEGER NOT NULL, - "course_code" TEXT NOT NULL, - "author_name" TEXT NOT NULL, - "title" TEXT NOT NULL, - "description" TEXT, - "term_taken" TEXT NOT NULL, - "created_timestamp" TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "upvotes" TEXT[], - "overall_rating" DOUBLE PRECISION NOT NULL, - - CONSTRAINT "pk_review_scraped_id" PRIMARY KEY ("review_scraped_id") -); - --- AddForeignKey -ALTER TABLE "reviews_scraped" ADD CONSTRAINT "fk_course_code" FOREIGN KEY ("course_code") REFERENCES "courses"("course_code") ON DELETE NO ACTION ON UPDATE NO ACTION; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 8aa64b0bf..623e339d6 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -15,27 +15,26 @@ enum report_status { } model courses { - courseCode String @id(map: "pk_course_code") @map("course_code") - archived Boolean - attributes String[] - calendar String - campus String - description String - enrolmentRules String @map("enrolment_rules") - equivalents String[] - exclusions String[] - faculty String - fieldOfEducation String @map("field_of_education") - genEd Boolean @map("gen_ed") - level Int - school String - studyLevel String @map("study_level") - terms Int[] - title String - uoc Int - rating Float - reviews reviews[] - reviewsScraped reviewsScraped[] + courseCode String @id(map: "pk_course_code") @map("course_code") + archived Boolean + attributes String[] + calendar String + campus String + description String + enrolmentRules String @map("enrolment_rules") + equivalents String[] + exclusions String[] + faculty String + fieldOfEducation String @map("field_of_education") + genEd Boolean @map("gen_ed") + level Int + school String + studyLevel String @map("study_level") + terms Int[] + title String + uoc Int + rating Float + reviews reviews[] } model reports { @@ -71,23 +70,6 @@ model reviews { users users @relation(fields: [zid], references: [zid], onDelete: NoAction, onUpdate: NoAction, map: "fk_zid") } -model reviewsScraped { - reviewId String @id(map: "pk_review_scraped_id") @default(dbgenerated("gen_random_uuid()")) @map("review_scraped_id") @db.Uuid - source String - sourceId Int @map("source_id") - courseCode String @map("course_code") - authorName String @map("author_name") - title String - description String? - termTaken String @map("term_taken") - createdTimestamp DateTime @default(now()) @map("created_timestamp") @db.Timestamp(6) - upvotes String[] - overallRating Float @map("overall_rating") - courses courses @relation(fields: [courseCode], references: [courseCode], onDelete: NoAction, onUpdate: NoAction, map: "fk_course_code") - - @@map("reviews_scraped") -} - model users { zid String @id(map: "pk_zid") bookmarkedReviews String[] @map("bookmarked_reviews") diff --git a/backend/src/api/schemas/review.schema.ts b/backend/src/api/schemas/review.schema.ts index 9a1ee23dd..16723b5d6 100644 --- a/backend/src/api/schemas/review.schema.ts +++ b/backend/src/api/schemas/review.schema.ts @@ -1,4 +1,3 @@ -import { title } from "process"; import { z } from "zod"; const CommonReviewSchema = z @@ -38,7 +37,6 @@ export const BookmarkReviewSchema = z reviewId: z.string(), zid: z.string(), bookmark: z.boolean(), - scraped: z.boolean(), }) .strict(); @@ -49,7 +47,6 @@ export const UpvoteReviewSchema = z reviewId: z.string(), zid: z.string(), upvote: z.boolean(), - scraped: z.boolean(), }) .strict(); @@ -121,49 +118,3 @@ const ReviewsSuccessResponseSchema = z export type ReviewsSuccessResponse = z.infer< typeof ReviewsSuccessResponseSchema >; - -export const ReviewScrapedSchema = z - .object({ - reviewId: z.string(), - source: z.string(), - sourceId: z.number(), - courseCode: z.string(), - authorName: z.string(), - title: z.string(), - description: z.string().nullable(), - termTaken: z.string(), - createdTimestamp: z.date(), - upvotes: z.string().array(), - overallRating: z.number(), - }) - .strict(); - -const ReviewScrapedSuccessResponseSchema = z - .object({ - review: z.array(ReviewScrapedSchema), - }) - .strict(); - -export type ReviewScrapedSuccessResponse = z.infer< - typeof ReviewScrapedSuccessResponseSchema ->; - -const ReviewsScrapedSuccessResponseSchema = z - .object({ - reviews: z.array(ReviewScrapedSchema), - }) - .strict(); - -export type ReviewsScrapedSuccessResponse = z.infer< - typeof ReviewsScrapedSuccessResponseSchema -> - -const AllReviewSuccessResponseSchema = z - .object({ - review: z.union([ReviewSchema, ReviewScrapedSchema]), - }) - .strict() - -export type AllReviewSuccessResponse = z.infer< - typeof AllReviewSuccessResponseSchema -> \ No newline at end of file diff --git a/backend/src/controllers/course.controller.ts b/backend/src/controllers/course.controller.ts index 46b48fbed..5582fd2e4 100644 --- a/backend/src/controllers/course.controller.ts +++ b/backend/src/controllers/course.controller.ts @@ -33,24 +33,6 @@ export class CourseController implements IController { } }, ) - .get( - "/courses/code/all", - async (req: Request, res: Response, next: NextFunction) => { - this.logger.debug(`Received request in GET /courses/code/all`); - try { - const allCourses = await this.courseService.getAllCourseCodes(); - this.logger.info(`Responding to client in GET /courses/code/all`); - return res.status(200).json(allCourses); - } catch (err: any) { - this.logger.warn( - `An error occurred when trying to GET /courses/code/all ${formatError( - err, - )}`, - ); - return next(err); - } - }, - ) .get( "/courses", async (req: Request, res: Response, next: NextFunction) => { diff --git a/backend/src/controllers/review.controller.ts b/backend/src/controllers/review.controller.ts index 6a53690cf..648aca8d3 100644 --- a/backend/src/controllers/review.controller.ts +++ b/backend/src/controllers/review.controller.ts @@ -34,8 +34,7 @@ export class ReviewController implements IController { this.logger.debug(`Received request in /reviews`); try { const result = await this.reviewService.getAllReviews(); - this.logger.info(`Responding to client in GET /reviews`); - return res.status(200).json({ ...result }); + return res.status(200).json(result); } catch (err: any) { this.logger.warn( `An error occurred when trying to GET /reviews ${formatError( @@ -47,25 +46,7 @@ export class ReviewController implements IController { }, ) .get( - "/reviews/scraped", - async (req: Request, res: Response, next: NextFunction) => { - this.logger.debug(`Received request in /reviews/scraped`); - try { - const result = await this.reviewService.getAllReviewsScraped(); - this.logger.info(`Responding to client in GET /reviews/scraped`); - return res.status(200).json({ ...result }); - } catch (err: any) { - this.logger.warn( - `An error occurred when trying to GET /reviews/scraped ${formatError( - err, - )}`, - ); - return next(err); - } - }, - ) - .get( - "/wrapped/reviews/most-liked", + "/wrapped/reviews/most-liked", async (req: Request, res: Response, next: NextFunction) => { this.logger.debug(`Received request in /wrapped/reviews/most-liked`); try { @@ -99,7 +80,7 @@ export class ReviewController implements IController { this.logger.info( `Responding to client in GET /reviews/${courseCode}`, ); - return res.status(200).json({ ...result }); + return res.status(200).json(result); } catch (err: any) { this.logger.warn( `An error occurred when trying to GET /reviews ${formatError( @@ -110,62 +91,7 @@ export class ReviewController implements IController { } }, ) - .get( - "/reviews/scraped/:courseCode", - async ( - req: Request<{ courseCode: string }, unknown>, - res: Response, - next: NextFunction, - ) => { - this.logger.debug( - `Received request in /reviews/scraped/:courseCode`, - ); - try { - const courseCode: string = req.params.courseCode; - const result = - await this.reviewService.getCourseReviewsScraped(courseCode); - this.logger.info( - `Responding to client in GET /reviews/scraped/${courseCode}`, - ); - return res.status(200).json({ ...result }); - } catch (err: any) { - this.logger.warn( - `An error occurred when trying to GET /reviews/scraped ${formatError( - err, - )}`, - ); - return next(err); - } - }, - ) - .get( - "/reviews/scraped/maxId/:source", - async ( - req: Request<{ source: string }, unknown>, - res: Response, - next: NextFunction, - ) => { - this.logger.debug( - `Received request in /reviews/scraped/maxId/:source`, - ); - try { - const source: string = req.params.source; - const result = - await this.reviewService.getSourceReviewScrapedMaxId(source); - this.logger.info( - `Responding to client in GET /reviews/scraped/maxId/${source}`, - ); - return res.status(200).json({ ...result }); - } catch (err: any) { - this.logger.warn( - `An error occurred when trying to GET /reviews/scraped/maxId ${formatError( - err, - )}`, - ); - return next(err); - } - }, - ) + .post( "/reviews", [verifyToken, validationMiddleware(PostReviewSchema, "body")], diff --git a/backend/src/repositories/course.repository.ts b/backend/src/repositories/course.repository.ts index 2bc576dd6..620ff19a7 100644 --- a/backend/src/repositories/course.repository.ts +++ b/backend/src/repositories/course.repository.ts @@ -37,13 +37,7 @@ export class CourseRepository { AVG(r.enjoyability) AS "enjoyability", CAST(COUNT(r.review_id) AS INT) AS "reviewCount" FROM courses c - LEFT JOIN - ( - SELECT course_code, review_id, overall_rating, manageability, usefulness, enjoyability FROM unilectives.reviews - UNION ALL - SELECT course_code, review_scraped_id, overall_rating, NULL AS manageability, NULL AS usefulness, NULL AS enjoyability FROM unilectives.reviews_scraped - ) - AS r USING(course_code) + LEFT JOIN reviews r ON c.course_code = r.course_code GROUP BY c.course_code ORDER BY "reviewCount" DESC `) as any[]; @@ -51,20 +45,6 @@ export class CourseRepository { return courses; } - async getAllCourseCodes(): Promise { - const rawCourses = await this.prisma.courses.findMany({ - select: { - courseCode: true, - }, - }); - - const courses = rawCourses.map((course) => - CourseCodeSchema.parse(course.courseCode), - ); - - return courses; - } - async getCoursesFromOffset(offset: number): Promise { const courses = (await this.prisma.$queryRaw` SELECT @@ -92,13 +72,7 @@ export class CourseRepository { AVG(r.enjoyability) AS "enjoyability", CAST(COUNT(r.review_id) AS INT) AS "reviewCount" FROM courses c - LEFT JOIN - ( - SELECT course_code, review_id, overall_rating, manageability, usefulness, enjoyability FROM unilectives.reviews - UNION ALL - SELECT course_code, review_scraped_id, overall_rating, NULL AS manageability, NULL AS usefulness, NULL AS enjoyability FROM unilectives.reviews_scraped - ) - AS r USING(course_code) + LEFT JOIN reviews r ON c.course_code = r.course_code GROUP BY c.course_code ORDER BY "reviewCount" DESC, c.course_code ASC LIMIT 25 OFFSET ${offset}; @@ -135,13 +109,7 @@ export class CourseRepository { AVG(r.enjoyability) AS "enjoyability", CAST(COUNT(r.review_id) AS INT) AS "reviewCount" FROM courses c - LEFT JOIN - ( - SELECT course_code, review_id, overall_rating, manageability, usefulness, enjoyability FROM unilectives.reviews - UNION ALL - SELECT course_code, review_scraped_id, overall_rating, NULL AS manageability, NULL AS usefulness, NULL AS enjoyability FROM unilectives.reviews_scraped - ) - AS r USING(course_code) + LEFT JOIN reviews r ON c.course_code = r.course_code WHERE c.course_code IN (${courseCodesString}) GROUP BY c.course_code ORDER BY "reviewCount" DESC; @@ -178,13 +146,7 @@ export class CourseRepository { AVG(r.enjoyability) AS "enjoyability", CAST(COUNT(r.review_id) AS INT) AS "reviewCount" FROM courses c - LEFT JOIN - ( - SELECT course_code, review_id, overall_rating, manageability, usefulness, enjoyability FROM unilectives.reviews - UNION ALL - SELECT course_code, review_scraped_id, overall_rating, NULL AS manageability, NULL AS usefulness, NULL AS enjoyability FROM unilectives.reviews_scraped - ) - AS r USING(course_code) + LEFT JOIN reviews r ON c.course_code = r.course_code WHERE c.course_code = '${courseCode}' GROUP BY c.course_code ORDER BY "reviewCount" DESC; @@ -221,13 +183,7 @@ export class CourseRepository { AVG(r.enjoyability) AS "enjoyability", CAST(COUNT(r.review_id) AS INT) AS "reviewCount" FROM courses c - LEFT JOIN - ( - SELECT course_code, review_id, overall_rating, manageability, usefulness, enjoyability FROM unilectives.reviews - UNION ALL - SELECT course_code, review_scraped_id, overall_rating, NULL AS manageability, NULL AS usefulness, NULL AS enjoyability FROM unilectives.reviews_scraped - ) - AS r USING(course_code) + LEFT JOIN reviews r ON c.course_code = r.course_code WHERE c.course_code ILIKE ${searchQuery} OR c.title ILIKE ${searchQuery} GROUP BY c.course_code ORDER BY @@ -307,13 +263,7 @@ export class CourseRepository { AVG(r.enjoyability) AS "enjoyability", CAST(COUNT(r.review_id) AS INT) AS "reviewCount" FROM courses c - LEFT JOIN - ( - SELECT course_code, review_id, overall_rating, manageability, usefulness, enjoyability FROM unilectives.reviews - UNION ALL - SELECT course_code, review_scraped_id, overall_rating, NULL AS manageability, NULL AS usefulness, NULL AS enjoyability FROM unilectives.reviews_scraped - ) - AS r USING(course_code) + LEFT JOIN reviews r ON c.course_code = r.course_code WHERE (c.course_code ILIKE ${searchQuery} OR c.title ILIKE ${searchQuery}) AND c.terms && ${termFilters}::integer[] AND c.faculty ILIKE ANY(${facultyFilters}) @@ -392,13 +342,7 @@ export class CourseRepository { AVG(r.enjoyability) AS "enjoyability", CAST(COUNT(r.review_id) AS INT) AS "reviewCount" FROM courses c - LEFT JOIN - ( - SELECT course_code, review_id, overall_rating, manageability, usefulness, enjoyability FROM unilectives.reviews - UNION ALL - SELECT course_code, review_scraped_id, overall_rating, NULL AS manageability, NULL AS usefulness, NULL AS enjoyability FROM unilectives.reviews_scraped - ) - AS r USING(course_code) + LEFT JOIN reviews r ON c.course_code = r.course_code WHERE (c.course_code ILIKE ${searchQuery} OR c.title ILIKE ${searchQuery}) AND (c.terms = ARRAY[]::integer[] OR c.terms && ${termFilters}::integer[]) AND c.faculty ILIKE ANY(${facultyFilters}) diff --git a/backend/src/repositories/review.repository.ts b/backend/src/repositories/review.repository.ts index d37e5e00e..d8753543d 100644 --- a/backend/src/repositories/review.repository.ts +++ b/backend/src/repositories/review.repository.ts @@ -1,4 +1,4 @@ -import { PrismaClient, reviews, reviewsScraped } from "@prisma/client"; +import { PrismaClient, reviews } from "@prisma/client"; import { PostReviewRequestBody, ReviewSchema, @@ -11,20 +11,6 @@ export class ReviewRepository { return await this.prisma.reviews.findMany(); } - async getAllReviewsScraped(): Promise { - return await this.prisma.reviewsScraped.findMany(); - } - - async getCourseReviewsScraped( - courseCode: string, - ): Promise { - return await this.prisma.reviewsScraped.findMany({ - where: { - courseCode, - }, - }); - } - async getCourseReviews(courseCode: string): Promise { return await this.prisma.reviews.findMany({ where: { @@ -68,17 +54,6 @@ export class ReviewRepository { }); } - async updateScrapedUpvotes(review: { reviewId: string; upvotes: string[] }) { - return await this.prisma.reviewsScraped.update({ - where: { - reviewId: review.reviewId, - }, - data: { - upvotes: review.upvotes, - }, - }); - } - async getReviewsByUser(zid: string): Promise { return await this.prisma.reviews.findMany({ where: { @@ -105,25 +80,6 @@ export class ReviewRepository { }); } - async getReviewScraped(reviewId: string): Promise { - return await this.prisma.reviewsScraped.findUnique({ - where: { - reviewId: reviewId, - }, - }); - } - - async getSourceReviewScrapedMaxId(source: string) { - return await this.prisma.reviewsScraped.aggregate({ - where: { - source: source - }, - _max: { - sourceId: true - } - }) - } - async deleteReview(reviewId: string) { return await this.prisma.reviews.delete({ where: { diff --git a/backend/src/services/course.service.ts b/backend/src/services/course.service.ts index 657e1aead..96c8f8f9a 100644 --- a/backend/src/services/course.service.ts +++ b/backend/src/services/course.service.ts @@ -33,17 +33,6 @@ export class CourseService { return { courses }; } - async getAllCourseCodes(): Promise { - const courseCodes = await this.courseRepository.getAllCourseCodes(); - if (courseCodes.length === 0) { - this.logger.error("Database returned with no course codes."); - throw new HTTPError(internalServerError); - } - - this.logger.info(`Found ${courseCodes.length} course codes.`); - return courseCodes; - } - async getCoursesFromOffset( offset: number, ): Promise { diff --git a/backend/src/services/review.service.test.ts b/backend/src/services/review.service.test.ts index f33b8667f..0375c5a67 100644 --- a/backend/src/services/review.service.test.ts +++ b/backend/src/services/review.service.test.ts @@ -141,7 +141,6 @@ describe("ReviewService", () => { reviewId: reviews.reviewId, zid: reviews.zid, bookmark: true, - scraped: false }; const errorResult = new HTTPError(badRequest); @@ -159,7 +158,6 @@ describe("ReviewService", () => { reviewId: reviews[0].reviewId, zid: reviews[0].zid, bookmark: true, - scraped: false }; const errorResult = new HTTPError(badRequest); @@ -179,7 +177,6 @@ describe("ReviewService", () => { reviewId: reviews[0].reviewId, zid: reviews[0].zid, bookmark: true, - scraped: false }; expect(service.bookmarkReview(request)).resolves.toEqual({ @@ -200,7 +197,6 @@ describe("ReviewService", () => { reviewId: reviews[0].reviewId, zid: reviews[0].zid, bookmark: false, - scraped: false }; expect(service.bookmarkReview(request)).resolves.toEqual({ review: reviews[0], diff --git a/backend/src/services/review.service.ts b/backend/src/services/review.service.ts index f05bf64b9..b7aef1b43 100644 --- a/backend/src/services/review.service.ts +++ b/backend/src/services/review.service.ts @@ -9,13 +9,11 @@ import { PostReviewRequestBody, PutReviewRequestBody, Review, - ReviewSuccessResponse, ReviewsSuccessResponse, - ReviewsScrapedSuccessResponse, - AllReviewSuccessResponse, + ReviewSuccessResponse, UpvoteReview, } from "../api/schemas/review.schema"; -import { reviews, reviewsScraped } from "@prisma/client"; +import { reviews } from "@prisma/client"; export class ReviewService { private logger = getLogger(); @@ -36,20 +34,6 @@ export class ReviewService { }; } - async getAllReviewsScraped(): Promise< - ReviewsScrapedSuccessResponse | undefined - > { - const reviews: reviewsScraped[] = - await this.reviewRepository.getAllReviewsScraped(); - if (reviews.length === 0) { - this.logger.error("Database returned with no reviews."); - throw new HTTPError(internalServerError); - } - return { - reviews: reviews, - }; - } - async getCourseReviews( courseCode: string, ): Promise { @@ -78,47 +62,6 @@ export class ReviewService { }; } - async getCourseReviewsScraped( - courseCode: string, - ): Promise { - let reviews = await this.redis.get( - `reviewsScraped:${courseCode}`, - ); - if (!reviews) { - this.logger.info(`Cache miss on reviewsScraped:${courseCode}`); - reviews = - await this.reviewRepository.getCourseReviewsScraped(courseCode); - await this.redis.set(`reviewsScraped:${courseCode}`, reviews); - } else { - this.logger.info(`Cache hit on reviewsScraped:${courseCode}`); - } - - if (reviews.length === 0) { - this.logger.error("Database returned with no reviews."); - throw new HTTPError(internalServerError); - } - this.logger.info(`Found ${reviews.length} reviews.`); - return { - reviews: reviews.map((review) => { - return { - ...review, - courseCode, - }; - }), - }; - } - - async getSourceReviewScrapedMaxId(source: string) { - const res = await this.reviewRepository.getSourceReviewScrapedMaxId(source); - let maxId = 0; - if (res._max && res._max.sourceId) { - maxId = res._max.sourceId; - } - return { - maxId: maxId - } - } - async postReview( reviewDetails: PostReviewRequestBody, ): Promise { @@ -192,11 +135,10 @@ export class ReviewService { async bookmarkReview( reviewDetails: BookmarkReview, - ): Promise { - - const review = reviewDetails.scraped ? - await this.reviewRepository.getReviewScraped(reviewDetails.reviewId) : - await this.reviewRepository.getReview(reviewDetails.reviewId) ; + ): Promise { + const review = await this.reviewRepository.getReview( + reviewDetails.reviewId, + ); if (!review) { this.logger.error( @@ -242,9 +184,7 @@ export class ReviewService { } async upvoteReview(upvoteDetails: UpvoteReview) { - let review = upvoteDetails.scraped ? - await this.reviewRepository.getReviewScraped(upvoteDetails.reviewId) : - await this.reviewRepository.getReview(upvoteDetails.reviewId) ; + let review = await this.reviewRepository.getReview(upvoteDetails.reviewId); if (!review) { this.logger.error( @@ -269,19 +209,13 @@ export class ReviewService { ); } - review = upvoteDetails.scraped ? - await this.reviewRepository.updateScrapedUpvotes(review) : - await this.reviewRepository.updateUpvotes(review); + review = await this.reviewRepository.updateUpvotes(review); - const reviews = upvoteDetails.scraped ? - await this.reviewRepository.getCourseReviewsScraped(review.courseCode) : - await this.reviewRepository.getCourseReviews(review.courseCode); + const reviews = await this.reviewRepository.getCourseReviews( + review.courseCode, + ); - if (upvoteDetails.scraped) { - await this.redis.set(`reviewsScraped:${review.courseCode}`, reviews); - } else { - await this.redis.set(`reviews:${review.courseCode}`, reviews); - } + await this.redis.set(`reviews:${review.courseCode}`, reviews); this.logger.info( `Successfully ${ diff --git a/backend/tsconfig.json b/backend/tsconfig.json index a09ba0af0..58ea5e562 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -23,8 +23,11 @@ "strict": true /* Enable all strict type-checking options. */, "skipLibCheck": true /* Skip type checking all .d.ts files. */, "strictPropertyInitialization": false, - "resolveJsonModule": true, + "resolveJsonModule": true }, - "include": ["src/**/*", "config/*"], + "include": [ + "src/**/*", + "config/*", + ], "exclude": ["src/**/*.test.ts"] } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 09b64d961..aa9765192 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,7 +13,6 @@ "@types/node": "18.15.11", "@types/react": "18.0.31", "@types/react-dom": "18.0.11", - "antd": "^5.24.1", "date-fns": "^2.30.0", "lodash": "^4.17.21", "next": "^13.4.4", @@ -48,117 +47,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ant-design/colors": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.0.tgz", - "integrity": "sha512-bjTObSnZ9C/O8MB/B4OUtd/q9COomuJAR2SYfhxLyHvCKn4EKwCN3e+fWGMo7H5InAyV0wL17jdE9ALrdOW/6A==", - "dependencies": { - "@ant-design/fast-color": "^2.0.6" - } - }, - "node_modules/@ant-design/cssinjs": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.23.0.tgz", - "integrity": "sha512-7GAg9bD/iC9ikWatU9ym+P9ugJhi/WbsTWzcKN6T4gU0aehsprtke1UAaaSxxkjjmkJb3llet/rbUSLPgwlY4w==", - "dependencies": { - "@babel/runtime": "^7.11.1", - "@emotion/hash": "^0.8.0", - "@emotion/unitless": "^0.7.5", - "classnames": "^2.3.1", - "csstype": "^3.1.3", - "rc-util": "^5.35.0", - "stylis": "^4.3.4" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/@ant-design/cssinjs-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", - "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==", - "dependencies": { - "@ant-design/cssinjs": "^1.21.0", - "@babel/runtime": "^7.23.2", - "rc-util": "^5.38.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@ant-design/fast-color": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", - "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", - "dependencies": { - "@babel/runtime": "^7.24.7" - }, - "engines": { - "node": ">=8.x" - } - }, - "node_modules/@ant-design/icons": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", - "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", - "dependencies": { - "@ant-design/colors": "^7.0.0", - "@ant-design/icons-svg": "^4.4.0", - "@babel/runtime": "^7.24.8", - "classnames": "^2.2.6", - "rc-util": "^5.31.1" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/@ant-design/icons-svg": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", - "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==" - }, - "node_modules/@ant-design/react-slick": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", - "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", - "dependencies": { - "@babel/runtime": "^7.10.4", - "classnames": "^2.2.5", - "json2mq": "^0.2.0", - "resize-observer-polyfill": "^1.5.1", - "throttle-debounce": "^5.0.0" - }, - "peerDependencies": { - "react": ">=16.9.0" - } - }, "node_modules/@babel/runtime": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", - "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", + "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", "dependencies": { - "regenerator-runtime": "^0.14.0" + "regenerator-runtime": "^0.13.11" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@emotion/hash": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" - }, - "node_modules/@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, "node_modules/@eslint-community/regexpp": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", @@ -613,146 +512,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/@rc-component/async-validator": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", - "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==", - "dependencies": { - "@babel/runtime": "^7.24.4" - }, - "engines": { - "node": ">=14.x" - } - }, - "node_modules/@rc-component/color-picker": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", - "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", - "dependencies": { - "@ant-design/fast-color": "^2.0.6", - "@babel/runtime": "^7.23.6", - "classnames": "^2.2.6", - "rc-util": "^5.38.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/context": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", - "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "rc-util": "^5.27.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/mini-decimal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", - "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", - "dependencies": { - "@babel/runtime": "^7.18.0" - }, - "engines": { - "node": ">=8.x" - } - }, - "node_modules/@rc-component/mutate-observer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", - "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", - "dependencies": { - "@babel/runtime": "^7.18.0", - "classnames": "^2.3.2", - "rc-util": "^5.24.4" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/portal": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", - "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", - "dependencies": { - "@babel/runtime": "^7.18.0", - "classnames": "^2.3.2", - "rc-util": "^5.24.4" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/qrcode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.0.0.tgz", - "integrity": "sha512-L+rZ4HXP2sJ1gHMGHjsg9jlYBX/SLN2D6OxP9Zn3qgtpMWtO2vUfxVFwiogHpAIqs54FnALxraUy/BCO1yRIgg==", - "dependencies": { - "@babel/runtime": "^7.24.7", - "classnames": "^2.3.2", - "rc-util": "^5.38.0" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/tour": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", - "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", - "dependencies": { - "@babel/runtime": "^7.18.0", - "@rc-component/portal": "^1.0.0-9", - "@rc-component/trigger": "^2.0.0", - "classnames": "^2.3.2", - "rc-util": "^5.24.4" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/@rc-component/trigger": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.6.tgz", - "integrity": "sha512-/9zuTnWwhQ3S3WT1T8BubuFTT46kvnXgaERR9f4BTKyn61/wpf/BvbImzYBubzJibU707FxwbKszLlHjcLiv1Q==", - "dependencies": { - "@babel/runtime": "^7.23.2", - "@rc-component/portal": "^1.1.0", - "classnames": "^2.3.2", - "rc-motion": "^2.0.0", - "rc-resize-observer": "^1.3.1", - "rc-util": "^5.44.0" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, "node_modules/@rushstack/eslint-patch": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.2.tgz", @@ -974,70 +733,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/antd": { - "version": "5.24.1", - "resolved": "https://registry.npmjs.org/antd/-/antd-5.24.1.tgz", - "integrity": "sha512-RGwpXpSr2RtoUnrpJl3V6ZaTExwSXkFVxV24VUowwC04n6oA1sGyJrofQOKNqD623sVxL5UJBmf0a+BFBImP3Q==", - "dependencies": { - "@ant-design/colors": "^7.2.0", - "@ant-design/cssinjs": "^1.23.0", - "@ant-design/cssinjs-utils": "^1.1.3", - "@ant-design/fast-color": "^2.0.6", - "@ant-design/icons": "^5.6.1", - "@ant-design/react-slick": "~1.1.2", - "@babel/runtime": "^7.26.0", - "@rc-component/color-picker": "~2.0.1", - "@rc-component/mutate-observer": "^1.1.0", - "@rc-component/qrcode": "~1.0.0", - "@rc-component/tour": "~1.15.1", - "@rc-component/trigger": "^2.2.6", - "classnames": "^2.5.1", - "copy-to-clipboard": "^3.3.3", - "dayjs": "^1.11.11", - "rc-cascader": "~3.33.0", - "rc-checkbox": "~3.5.0", - "rc-collapse": "~3.9.0", - "rc-dialog": "~9.6.0", - "rc-drawer": "~7.2.0", - "rc-dropdown": "~4.2.1", - "rc-field-form": "~2.7.0", - "rc-image": "~7.11.0", - "rc-input": "~1.7.2", - "rc-input-number": "~9.4.0", - "rc-mentions": "~2.19.1", - "rc-menu": "~9.16.0", - "rc-motion": "^2.9.5", - "rc-notification": "~5.6.3", - "rc-pagination": "~5.1.0", - "rc-picker": "~4.11.1", - "rc-progress": "~4.0.0", - "rc-rate": "~2.13.1", - "rc-resize-observer": "^1.4.3", - "rc-segmented": "~2.7.0", - "rc-select": "~14.16.6", - "rc-slider": "~11.1.8", - "rc-steps": "~6.0.1", - "rc-switch": "~4.1.0", - "rc-table": "~7.50.3", - "rc-tabs": "~15.5.1", - "rc-textarea": "~1.9.0", - "rc-tooltip": "~6.4.0", - "rc-tree": "~5.13.0", - "rc-tree-select": "~5.27.0", - "rc-upload": "~4.8.1", - "rc-util": "^5.44.4", - "scroll-into-view-if-needed": "^3.1.0", - "throttle-debounce": "^5.0.2" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ant-design" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1535,11 +1230,6 @@ "node": ">= 6" } }, - "node_modules/classnames": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -1571,11 +1261,6 @@ "node": ">= 6" } }, - "node_modules/compute-scroll-into-view": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", - "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1589,14 +1274,6 @@ "node": ">= 0.6" } }, - "node_modules/copy-to-clipboard": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", - "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", - "dependencies": { - "toggle-selection": "^1.0.6" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -1623,9 +1300,9 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -1699,11 +1376,6 @@ "url": "https://opencollective.com/date-fns" } }, - "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3708,14 +3380,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/json2mq": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", - "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", - "dependencies": { - "string-convert": "^0.2.0" - } - }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -4676,583 +4340,6 @@ } ] }, - "node_modules/rc-cascader": { - "version": "3.33.0", - "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.33.0.tgz", - "integrity": "sha512-JvZrMbKBXIbEDmpIORxqvedY/bck6hGbs3hxdWT8eS9wSQ1P7//lGxbyKjOSyQiVBbgzNWriSe6HoMcZO/+0rQ==", - "dependencies": { - "@babel/runtime": "^7.25.7", - "classnames": "^2.3.1", - "rc-select": "~14.16.2", - "rc-tree": "~5.13.0", - "rc-util": "^5.43.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-checkbox": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.5.0.tgz", - "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.3.2", - "rc-util": "^5.25.2" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-collapse": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.9.0.tgz", - "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "2.x", - "rc-motion": "^2.3.4", - "rc-util": "^5.27.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-dialog": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", - "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/portal": "^1.0.0-8", - "classnames": "^2.2.6", - "rc-motion": "^2.3.0", - "rc-util": "^5.21.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-drawer": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.2.0.tgz", - "integrity": "sha512-9lOQ7kBekEJRdEpScHvtmEtXnAsy+NGDXiRWc2ZVC7QXAazNVbeT4EraQKYwCME8BJLa8Bxqxvs5swwyOepRwg==", - "dependencies": { - "@babel/runtime": "^7.23.9", - "@rc-component/portal": "^1.1.1", - "classnames": "^2.2.6", - "rc-motion": "^2.6.1", - "rc-util": "^5.38.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-dropdown": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.1.tgz", - "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "@rc-component/trigger": "^2.0.0", - "classnames": "^2.2.6", - "rc-util": "^5.44.1" - }, - "peerDependencies": { - "react": ">=16.11.0", - "react-dom": ">=16.11.0" - } - }, - "node_modules/rc-field-form": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.7.0.tgz", - "integrity": "sha512-hgKsCay2taxzVnBPZl+1n4ZondsV78G++XVsMIJCAoioMjlMQR9YwAp7JZDIECzIu2Z66R+f4SFIRrO2DjDNAA==", - "dependencies": { - "@babel/runtime": "^7.18.0", - "@rc-component/async-validator": "^5.0.3", - "rc-util": "^5.32.2" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-image": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.11.0.tgz", - "integrity": "sha512-aZkTEZXqeqfPZtnSdNUnKQA0N/3MbgR7nUnZ+/4MfSFWPFHZau4p5r5ShaI0KPEMnNjv4kijSCFq/9wtJpwykw==", - "dependencies": { - "@babel/runtime": "^7.11.2", - "@rc-component/portal": "^1.0.2", - "classnames": "^2.2.6", - "rc-dialog": "~9.6.0", - "rc-motion": "^2.6.2", - "rc-util": "^5.34.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-input": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.7.2.tgz", - "integrity": "sha512-g3nYONnl4edWj2FfVoxsU3Ec4XTE+Hb39Kfh2MFxMZjp/0gGyPUgy/v7ZhS27ZxUFNkuIDYXm9PJsLyJbtg86A==", - "dependencies": { - "@babel/runtime": "^7.11.1", - "classnames": "^2.2.1", - "rc-util": "^5.18.1" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/rc-input-number": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.4.0.tgz", - "integrity": "sha512-Tiy4DcXcFXAf9wDhN8aUAyMeCLHJUHA/VA/t7Hj8ZEx5ETvxG7MArDOSE6psbiSCo+vJPm4E3fGN710ITVn6GA==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/mini-decimal": "^1.0.1", - "classnames": "^2.2.5", - "rc-input": "~1.7.1", - "rc-util": "^5.40.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-mentions": { - "version": "2.19.1", - "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.19.1.tgz", - "integrity": "sha512-KK3bAc/bPFI993J3necmaMXD2reZTzytZdlTvkeBbp50IGH1BDPDvxLdHDUrpQx2b2TGaVJsn+86BvYa03kGqA==", - "dependencies": { - "@babel/runtime": "^7.22.5", - "@rc-component/trigger": "^2.0.0", - "classnames": "^2.2.6", - "rc-input": "~1.7.1", - "rc-menu": "~9.16.0", - "rc-textarea": "~1.9.0", - "rc-util": "^5.34.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-menu": { - "version": "9.16.1", - "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", - "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/trigger": "^2.0.0", - "classnames": "2.x", - "rc-motion": "^2.4.3", - "rc-overflow": "^1.3.1", - "rc-util": "^5.27.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-motion": { - "version": "2.9.5", - "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.5.tgz", - "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==", - "dependencies": { - "@babel/runtime": "^7.11.1", - "classnames": "^2.2.1", - "rc-util": "^5.44.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-notification": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.3.tgz", - "integrity": "sha512-42szwnn8VYQoT6GnjO00i1iwqV9D1TTMvxObWsuLwgl0TsOokzhkYiufdtQBsJMFjJravS1hfDKVMHLKLcPE4g==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "2.x", - "rc-motion": "^2.9.0", - "rc-util": "^5.20.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-overflow": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.4.1.tgz", - "integrity": "sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw==", - "dependencies": { - "@babel/runtime": "^7.11.1", - "classnames": "^2.2.1", - "rc-resize-observer": "^1.0.0", - "rc-util": "^5.37.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-pagination": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz", - "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.3.2", - "rc-util": "^5.38.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-picker": { - "version": "4.11.2", - "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.2.tgz", - "integrity": "sha512-Cwa3frWpefhESBF20HBJtvWx3q1hCrMxSUrzuuWMTGoZVPhQllGEp2IUfzo9jC5LKm4kJx7IrH8q/W/y9wClAw==", - "dependencies": { - "@babel/runtime": "^7.24.7", - "@rc-component/trigger": "^2.0.0", - "classnames": "^2.2.1", - "rc-overflow": "^1.3.2", - "rc-resize-observer": "^1.4.0", - "rc-util": "^5.43.0" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "date-fns": ">= 2.x", - "dayjs": ">= 1.x", - "luxon": ">= 3.x", - "moment": ">= 2.x", - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - }, - "peerDependenciesMeta": { - "date-fns": { - "optional": true - }, - "dayjs": { - "optional": true - }, - "luxon": { - "optional": true - }, - "moment": { - "optional": true - } - } - }, - "node_modules/rc-progress": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", - "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.2.6", - "rc-util": "^5.16.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-rate": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", - "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.2.5", - "rc-util": "^5.0.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-resize-observer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", - "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==", - "dependencies": { - "@babel/runtime": "^7.20.7", - "classnames": "^2.2.1", - "rc-util": "^5.44.1", - "resize-observer-polyfill": "^1.5.1" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-segmented": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.7.0.tgz", - "integrity": "sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA==", - "dependencies": { - "@babel/runtime": "^7.11.1", - "classnames": "^2.2.1", - "rc-motion": "^2.4.4", - "rc-util": "^5.17.0" - }, - "peerDependencies": { - "react": ">=16.0.0", - "react-dom": ">=16.0.0" - } - }, - "node_modules/rc-select": { - "version": "14.16.6", - "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.6.tgz", - "integrity": "sha512-YPMtRPqfZWOm2XGTbx5/YVr1HT0vn//8QS77At0Gjb3Lv+Lbut0IORJPKLWu1hQ3u4GsA0SrDzs7nI8JG7Zmyg==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/trigger": "^2.1.1", - "classnames": "2.x", - "rc-motion": "^2.0.1", - "rc-overflow": "^1.3.1", - "rc-util": "^5.16.1", - "rc-virtual-list": "^3.5.2" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/rc-slider": { - "version": "11.1.8", - "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.8.tgz", - "integrity": "sha512-2gg/72YFSpKP+Ja5AjC5DPL1YnV8DEITDQrcc1eASrUYjl0esptaBVJBh5nLTXCCp15eD8EuGjwezVGSHhs9tQ==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.2.5", - "rc-util": "^5.36.0" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-steps": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", - "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", - "dependencies": { - "@babel/runtime": "^7.16.7", - "classnames": "^2.2.3", - "rc-util": "^5.16.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-switch": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", - "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", - "dependencies": { - "@babel/runtime": "^7.21.0", - "classnames": "^2.2.1", - "rc-util": "^5.30.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-table": { - "version": "7.50.3", - "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.3.tgz", - "integrity": "sha512-Z4/zNCzjv7f/XzPRecb+vJU0DJKdsYt4YRkDzNl4G05m7JmxrKGYC2KqN1Ew6jw2zJq7cxVv3z39qyZOHMuf7A==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "@rc-component/context": "^1.4.0", - "classnames": "^2.2.5", - "rc-resize-observer": "^1.1.0", - "rc-util": "^5.44.3", - "rc-virtual-list": "^3.14.2" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-tabs": { - "version": "15.5.1", - "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.5.1.tgz", - "integrity": "sha512-yiWivLAjEo5d1v2xlseB2dQocsOhkoVSfo1krS8v8r+02K+TBUjSjXIf7dgyVSxp6wRIPv5pMi5hanNUlQMgUA==", - "dependencies": { - "@babel/runtime": "^7.11.2", - "classnames": "2.x", - "rc-dropdown": "~4.2.0", - "rc-menu": "~9.16.0", - "rc-motion": "^2.6.2", - "rc-resize-observer": "^1.0.0", - "rc-util": "^5.34.1" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-textarea": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.9.0.tgz", - "integrity": "sha512-dQW/Bc/MriPBTugj2Kx9PMS5eXCCGn2cxoIaichjbNvOiARlaHdI99j4DTxLl/V8+PIfW06uFy7kjfUIDDKyxQ==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "^2.2.1", - "rc-input": "~1.7.1", - "rc-resize-observer": "^1.0.0", - "rc-util": "^5.27.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-tooltip": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", - "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", - "dependencies": { - "@babel/runtime": "^7.11.2", - "@rc-component/trigger": "^2.0.0", - "classnames": "^2.3.1", - "rc-util": "^5.44.3" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-tree": { - "version": "5.13.0", - "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.13.0.tgz", - "integrity": "sha512-2+lFvoVRnvHQ1trlpXMOWtF8BUgF+3TiipG72uOfhpL5CUdXCk931kvDdUkTL/IZVtNEDQKwEEmJbAYJSA5NnA==", - "dependencies": { - "@babel/runtime": "^7.10.1", - "classnames": "2.x", - "rc-motion": "^2.0.1", - "rc-util": "^5.16.1", - "rc-virtual-list": "^3.5.1" - }, - "engines": { - "node": ">=10.x" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/rc-tree-select": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.27.0.tgz", - "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==", - "dependencies": { - "@babel/runtime": "^7.25.7", - "classnames": "2.x", - "rc-select": "~14.16.2", - "rc-tree": "~5.13.0", - "rc-util": "^5.43.0" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/rc-upload": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.8.1.tgz", - "integrity": "sha512-toEAhwl4hjLAI1u8/CgKWt30BR06ulPa4iGQSMvSXoHzO88gPCslxqV/mnn4gJU7PDoltGIC9Eh+wkeudqgHyw==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "classnames": "^2.2.5", - "rc-util": "^5.2.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-util": { - "version": "5.44.4", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", - "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", - "dependencies": { - "@babel/runtime": "^7.18.3", - "react-is": "^18.2.0" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, - "node_modules/rc-util/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" - }, - "node_modules/rc-virtual-list": { - "version": "3.18.2", - "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.18.2.tgz", - "integrity": "sha512-SkPabqstOQgJ2Q2Ob3eDPIHsNrDzQZFl8mzHiXuNablyYwddVU33Ws6oxoA7Fi/6pZeEYonrLEUiJGr/6aBVaw==", - "dependencies": { - "@babel/runtime": "^7.20.0", - "classnames": "^2.2.6", - "rc-resize-observer": "^1.0.0", - "rc-util": "^5.36.0" - }, - "engines": { - "node": ">=8.x" - }, - "peerDependencies": { - "react": ">=16.9.0", - "react-dom": ">=16.9.0" - } - }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -5336,9 +4423,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", @@ -5358,11 +4445,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" - }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -5598,14 +4680,6 @@ "typescript": ">=4.1.0" } }, - "node_modules/scroll-into-view-if-needed": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", - "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", - "dependencies": { - "compute-scroll-into-view": "^3.0.2" - } - }, "node_modules/semver": { "version": "7.5.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", @@ -5735,11 +4809,6 @@ "node": ">=10.0.0" } }, - "node_modules/string-convert": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", - "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==" - }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -5960,11 +5029,6 @@ } } }, - "node_modules/stylis": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", - "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==" - }, "node_modules/sucrase": { "version": "3.32.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", @@ -6154,14 +5218,6 @@ "node": ">=0.8" } }, - "node_modules/throttle-debounce": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", - "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", - "engines": { - "node": ">=12.22" - } - }, "node_modules/titleize": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", @@ -6185,11 +5241,6 @@ "node": ">=8.0" } }, - "node_modules/toggle-selection": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" - }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", diff --git a/frontend/package.json b/frontend/package.json index cdf0a3aab..48b0bef7d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,7 +15,6 @@ "@types/node": "18.15.11", "@types/react": "18.0.31", "@types/react-dom": "18.0.11", - "antd": "^5.24.1", "date-fns": "^2.30.0", "lodash": "^4.17.21", "next": "^13.4.4", diff --git a/frontend/src/app/course/[id]/page.tsx b/frontend/src/app/course/[id]/page.tsx index d9281afa9..5f9e3d371 100644 --- a/frontend/src/app/course/[id]/page.tsx +++ b/frontend/src/app/course/[id]/page.tsx @@ -4,7 +4,7 @@ import ReviewSearchbar from "@/components/ReviewSearchBar/ReviewSearchBar"; import ReviewsBar from "@/components/ReviewsBar/ReviewsBar"; import TermsGroup from "@/components/TermsGroup/TermsGroup"; import { authOptions } from "@/lib/auth"; -import { Course, Reviews, Review } from "@/types/api"; +import { Course, Reviews } from "@/types/api"; import { get, validatedReq } from "@/utils/request"; import { LinkIcon } from "@heroicons/react/24/solid"; import { Metadata } from "next"; @@ -53,15 +53,6 @@ export default async function ReviewPage({ `/reviews/${course.courseCode.toUpperCase()}`, )) as Reviews; - const { reviews: reviewsScraped } = (await get( - `/reviews/scraped/${course.courseCode.toUpperCase()}` - )) as Reviews; - - const allReviews: Review[] = [ - ...(reviews || []), - ...(reviewsScraped || []) - ]; - let userCourseInfo: string[] = []; if (session?.user) { try { @@ -230,7 +221,7 @@ export default async function ReviewPage({ Loading...
}> diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index f3c99e031..fb459b892 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -7,7 +7,6 @@ import { ItemList, WithContext } from "schema-dts"; import { get } from "@/utils/request"; import { Course, Courses } from "@/types/api"; - export async function generateMetadata(): Promise { return { title: `Home | Unilectives - UNSW Course Reviews`, diff --git a/frontend/src/components/EditReviewModal/EditReviewModal.tsx b/frontend/src/components/EditReviewModal/EditReviewModal.tsx index c24b468cd..244d3b341 100644 --- a/frontend/src/components/EditReviewModal/EditReviewModal.tsx +++ b/frontend/src/components/EditReviewModal/EditReviewModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { ReviewNative } from "@/types/api"; +import { Review } from "@/types/api"; import { Dialog, Transition } from "@headlessui/react"; import { PencilSquareIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { FormEvent, Fragment, useState } from "react"; @@ -14,7 +14,7 @@ export default function EditReviewModal({ review, setEdited, }: { - review: ReviewNative; + review: Review; setEdited: (detail: { reviewId: string; authorName: string; diff --git a/frontend/src/components/FilterModal/FilterModal.tsx b/frontend/src/components/FilterModal/FilterModal.tsx index 11b31b7ec..ce40b02b2 100644 --- a/frontend/src/components/FilterModal/FilterModal.tsx +++ b/frontend/src/components/FilterModal/FilterModal.tsx @@ -105,6 +105,15 @@ export default function FilterModal({ } }); + // Show secret message if specific filters are selected + if ( + selectedFaculties.includes("Arts") && + selectedTerms.includes("-1") && + selectedTerms.includes("-2") + ) { + alert("Alss aol zlhyjo ihy..."); + } + setFilters({ faculties: selectedFaculties, terms: selectedTerms }); setOpen(false); diff --git a/frontend/src/components/ReviewCard/ReviewCard.tsx b/frontend/src/components/ReviewCard/ReviewCard.tsx index 991264c31..8cd966f7d 100644 --- a/frontend/src/components/ReviewCard/ReviewCard.tsx +++ b/frontend/src/components/ReviewCard/ReviewCard.tsx @@ -36,7 +36,6 @@ export default function ReviewCard({ reviewId: review.reviewId, zid: session?.user?.id, upvote, - scraped: !('enjoyability' in review), // no scraped review has enjoyability }; await validatedReq( "POST", @@ -66,14 +65,13 @@ export default function ReviewCard({ reviewId: review.reviewId, zid: session?.user?.id, bookmark: !bookmarked, - scraped: !('enjoyability' in review), // no scraped review has enjoyability }; await validatedReq( "POST", "/reviews/bookmark", session?.user?.accessToken ?? "", session?.user?.id ?? "", - body, + body ); // Optimistic UI update for bookmark setAllBookmarkedReviews((prev: string[]) => { @@ -111,15 +109,12 @@ export default function ReviewCard({ {/* Term taken + Grade */}

Term taken: {review.termTaken}

- {'grade' in review && ( -

- Grade: {!review.grade ? "-" : review.grade} -

- )} +

+ Grade: {!review.grade ? "-" : review.grade} +

{/* Circle rating */} - {'enjoyability' in review && ( -
+
{/* Enjoyability */}

Enjoyment

@@ -148,7 +143,6 @@ export default function ReviewCard({ />
- )} {/* Description */} {/* Icons */} @@ -157,10 +151,12 @@ export default function ReviewCard({
); } diff --git a/frontend/src/components/UserPageContent/UserPageContent.tsx b/frontend/src/components/UserPageContent/UserPageContent.tsx index 723682376..84ed9b333 100644 --- a/frontend/src/components/UserPageContent/UserPageContent.tsx +++ b/frontend/src/components/UserPageContent/UserPageContent.tsx @@ -1,7 +1,7 @@ "use client"; -import { useCallback, useRef, useState } from "react"; -import { Course, Report, Review, ReviewNative, TabsType } from "@/types/api"; +import { Dispatch, SetStateAction, useCallback, useRef, useState } from "react"; +import { Course, Report, Review, TabsType } from "@/types/api"; import UserReviews from "../UserReviews/UserReviews"; import UserReports from "../UserReports/UserReports"; import Dropdown from "../Dropdown/Dropdown"; @@ -77,7 +77,7 @@ export default function UserPageContent({ {/* My reviews */} {tabs["My reviews"].current && ( )} diff --git a/frontend/src/components/UserReports/UserReports.tsx b/frontend/src/components/UserReports/UserReports.tsx index 298c33737..6141e827f 100644 --- a/frontend/src/components/UserReports/UserReports.tsx +++ b/frontend/src/components/UserReports/UserReports.tsx @@ -10,6 +10,7 @@ import { } from "react"; import Dropdown from "../Dropdown/Dropdown"; import Pagination from "../Pagination/Pagination"; +import ReviewCard from "../ReviewCard/ReviewCard"; type STATUS = { UNSEEN: boolean; @@ -182,11 +183,15 @@ export default function UserReports({ ))}
{/* Pagination */} - setPage(page)} - /> + {reports.length > 0 ? ( + setPage(page)} + /> + ) : ( +
No reports made yet.
+ )} ); } diff --git a/frontend/src/components/UserReviews/UserReviews.tsx b/frontend/src/components/UserReviews/UserReviews.tsx index 7074ade1f..9638d3193 100644 --- a/frontend/src/components/UserReviews/UserReviews.tsx +++ b/frontend/src/components/UserReviews/UserReviews.tsx @@ -1,6 +1,6 @@ "use client"; -import { ReviewNative, TabsType, ReviewsNative } from "@/types/api"; +import { Course, Review, Reviews, TabsType } from "@/types/api"; import Dropdown from "../Dropdown/Dropdown"; import { Dispatch, SetStateAction, useEffect, useState } from "react"; import Rating from "../Rating/Rating"; @@ -12,7 +12,7 @@ import RemoveReviewModal from "../RemoveReviewModal/RemoveReviewModal"; export default function UserReviews({ reviews, setTabs, -}: ReviewsNative & { +}: Reviews & { setTabs: Dispatch>; }) { const [selected, setSelected] = useState(""); @@ -32,23 +32,23 @@ export default function UserReviews({ switch (selected) { case "Most Recent": sortedReviews.sort( - (r1: ReviewNative, r2: ReviewNative) => + (r1: Review, r2: Review) => Date.parse(r2.createdTimestamp) - Date.parse(r1.createdTimestamp) ); break; case "Most Recently Taken": - sortedReviews.sort((r1: ReviewNative, r2: ReviewNative) => + sortedReviews.sort((r1: Review, r2: Review) => r2.termTaken.localeCompare(r1.termTaken) ); break; case "Highest Rating to Lowest Rating": sortedReviews.sort( - (r1: ReviewNative, r2: ReviewNative) => r2.overallRating - r1.overallRating + (r1: Review, r2: Review) => r2.overallRating - r1.overallRating ); break; case "Lowest Rating to Highest Rating": sortedReviews.sort( - (r1: ReviewNative, r2: ReviewNative) => r1.overallRating - r2.overallRating + (r1: Review, r2: Review) => r1.overallRating - r2.overallRating ); break; } @@ -123,7 +123,7 @@ export default function UserReviews({
{reviews .slice((page - 1) * itemPerPage, page * itemPerPage) - .map((review: ReviewNative, index: number) => ( + .map((review: Review, index: number) => (
{reviews .slice((page - 1) * itemPerPage, page * itemPerPage) - .map((review: ReviewNative, index: number) => ( + .map((review: Review, index: number) => (
)} {/* Pagination */} - setPage(page)} - /> + {reviews.length > 0 ? ( + setPage(page)} + /> + ) : ( +
No courses reviewed yet.
+ )}
); } diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts index 691e38bf9..d37dbeea5 100644 --- a/frontend/src/types/api.ts +++ b/frontend/src/types/api.ts @@ -3,9 +3,7 @@ export type ApiError = { errorMessage: string; }; -export type Review = ReviewNative | ReviewScraped; - -export type ReviewNative = { +export type Review = { reviewId: string; courseCode: string; authorName: string; @@ -22,26 +20,10 @@ export type ReviewNative = { overallRating: number; }; -export type ReviewScraped = { - reviewId: string; - courseCode: string; - authorName: string; - title: string; - description: string; - termTaken: string; - createdTimestamp: string; - upvotes: string[]; - overallRating: number; -}; - export type Reviews = { reviews: Review[]; }; -export type ReviewsNative = { - reviews: ReviewNative[]; -}; - export type ReportStatus = "UNSEEN" | "SEEN" | "REMOVED" | "SETTLED"; export type Report = { diff --git a/scraper/.gitignore b/scraper/.gitignore deleted file mode 100644 index 4f34dec4a..000000000 --- a/scraper/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -studentVIP_reviews.json -uninotes_reviews.json \ No newline at end of file diff --git a/scraper/studentVIP.py b/scraper/studentVIP.py deleted file mode 100644 index 4f102e511..000000000 --- a/scraper/studentVIP.py +++ /dev/null @@ -1,56 +0,0 @@ -from bs4 import BeautifulSoup -import requests - -website = "StudentVIP" - -# Get all course codes from localhost:3030/api/v1/courses/code/all -url = "http://localhost:3030/api/v1/courses/code/all" -response = requests.get(url) -courses = response.json() - -# Get latest studentvip review ID -start_id = requests.get("http://localhost:3030/api/v1/reviews/scraped/maxId/StudentVIP").json()['maxId'] - -course_codes = courses -url_prefix = "https://studentvip.com.au/unsw/subjects/" -review_values = [] -batch_size = 1000 -count = 0 - -for course_code in course_codes: - page = requests.get(url_prefix + course_code) - soup = BeautifulSoup(page.content, "html.parser") - - try: - res = soup.find("h3", class_="text-subjects") \ - .find_next(class_="list-group") \ - .find_all(class_="panel-body") - - for review in res: - rating = len(review.find_all("i", class_="fa fa-star")) - description = review.find("p").get_text(strip=True).replace("'", "''") # Escape single quotes - name, term, year = review.find("small").get_text(strip=True).split(",") - author_name = name.strip().replace("'", "''") # Escape single quotes - term_taken = term.split(' ') - year_taken = year.strip() - term_taken = year_taken[2:] + term_taken[1][0] + term_taken[2] - - print("Scraped review for course ", course_code) - count += 1 - cur_id = start_id + count - review_values.append( - f"('{course_code}', '{website}', '{cur_id}', 'Review #{cur_id}', {rating}, '{description}', '{author_name}', '{term_taken}', '{{}}')" - ) - - except Exception as e: - print(f"Could not process reviews for {course_code}: {str(e)}") - -# Write SQL statements to a file -with open('../backend/data/studentVIP_reviews.sql', 'w') as f: - f.write("-- StudentVIP Reviews SQL Import\n\n") - for i in range(0, len(review_values), batch_size): - batch = review_values[i:i+batch_size] - sql = f"INSERT INTO unilectives.reviews_scraped (course_code, source, source_id, title, overall_rating, description, author_name, term_taken, upvotes) VALUES {', '.join(batch)};\n" - f.write(sql) - -print(f"SQL file created with {len(review_values)} INSERT statements.") diff --git a/scraper/uniNotes.py b/scraper/uniNotes.py deleted file mode 100644 index 31be9159b..000000000 --- a/scraper/uniNotes.py +++ /dev/null @@ -1,77 +0,0 @@ -from bs4 import BeautifulSoup -import re -import requests -import signal -import time - -website = "UniNotes" - -# Get all course codes from localhost:3030/api/v1/courses/code/all -url = "http://localhost:3030/api/v1/courses/code/all" -response = requests.get(url) -courses = response.json() - -# Get latest studentvip review ID -start_id = requests.get(f"http://localhost:3030/api/v1/reviews/scraped/maxId/{website}").json()['maxId'] -course_codes = courses -url_prefix = "https://uninotes.com/university-subjects/university-of-new-south-wales-unsw/" -review_values = [] -count = 0 - -# Define a timeout handler -def handler(signum, frame): - raise TimeoutError("Loop iteration timed out") - -# Set the timeout duration (in seconds) in case the scraping takes too long -timeout_duration = 10 - -for course_code in course_codes: - signal.signal(signal.SIGALRM, handler) - signal.alarm(timeout_duration) - - try: - page = requests.get(url_prefix + course_code) - soup = BeautifulSoup(page.content, "html.parser") - res = soup.find_all(attrs={"id": re.compile(r'^review')}) - for review in res: - author_name = review.find("h2").get_text(strip=True).replace("'", "''") - - unformattedDesc = '\n'.join(review.find(class_="details").stripped_strings) - - description = re.search(r"Comments\n(.+?)\nContact Hours", unformattedDesc, re.DOTALL) - description = description.group(1).strip().replace("\n", " ").replace("'", "''") if description else "" - - rating = re.search(r"Overall Rating\n([0-9.]+)/5", unformattedDesc) - rating = float(rating.group(1)) if rating else 0 - - grade = re.search(r"Your Mark / Grade\n([\dA-Z ]+)", unformattedDesc) - grade = grade.group(1).strip().replace("'", "''") if grade else "Unknown" - - term_taken_match = re.search(r'Year & (Trimester|Semester) Of Completion\n(.+?)\nYour Mark / Grade', unformattedDesc) - term_taken = term_taken_match.group(2).replace("'", "''") if term_taken_match else "Unknown" - - count += 1 - cur_id = start_id + count - review_values.append( - f"('{course_code}', '{website}', '{cur_id}', 'Review #{cur_id}', {rating}, '{description}', '{author_name}', '{term_taken}', '{{}}')" - ) - if len(res) > 0: - print(f"Scraped reviews for {course_code}...") - except TimeoutError: - print(f"Iteration timed out, moving to next iteration.") - except Exception as e: - print(f"Could not process reviews for {course_code}: {str(e)}") - finally: - signal.alarm(0) # Disable the alarm after each iteration - -# Write SQL statements to a file -with open('../backend/data/uninotes_reviews.sql', 'w') as f: - f.write("-- UniNotes Reviews SQL Import\n\n") - - batch_size = 1000 - for i in range(0, len(review_values), batch_size): - batch = review_values[i:i+batch_size] - sql = f"INSERT INTO unilectives.reviews_scraped (course_code, source, source_id, title, overall_rating, description, author_name, term_taken, upvotes) VALUES {', '.join(batch)};\n" - f.write(sql) - -print(f"SQL file created with {len(review_values)} reviews in multi-insert statements.") \ No newline at end of file From d352688c0c515518d559edf92c0ea3c4da571a48 Mon Sep 17 00:00:00 2001 From: Auston Yang <163545426+ajzombie123@users.noreply.github.com> Date: Thu, 19 Jun 2025 23:47:00 +1000 Subject: [PATCH 3/3] Light and Dark mode Colour scheme changes (#360) added visual improvements to CourseCard, Navbar, ReviewCard and tailwind.config.js --- frontend/src/components/CourseCard/CourseCard.tsx | 2 +- frontend/src/components/Navbar/Navbar.tsx | 2 +- frontend/src/components/ReviewCard/ReviewCard.tsx | 2 +- frontend/tailwind.config.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/CourseCard/CourseCard.tsx b/frontend/src/components/CourseCard/CourseCard.tsx index 1711a594b..b9e871131 100644 --- a/frontend/src/components/CourseCard/CourseCard.tsx +++ b/frontend/src/components/CourseCard/CourseCard.tsx @@ -19,7 +19,7 @@ export default function CourseCard({ terms, }: CourseCardProps) { return ( -
+
{/* Course courseCode + Ratings */}

diff --git a/frontend/src/components/Navbar/Navbar.tsx b/frontend/src/components/Navbar/Navbar.tsx index aa4860d6a..7351fb7e4 100644 --- a/frontend/src/components/Navbar/Navbar.tsx +++ b/frontend/src/components/Navbar/Navbar.tsx @@ -67,7 +67,7 @@ export default function Navbar({ userZid }: NavbarProps) { ref={ref} className={ collapsed - ? "fixed flex flex-col items-center w-20 h-screen gap-4 p-4 duration-150 bg-gray-50 dark:bg-slate-700 z-50 xs:p-2 xs:w-15 xs:gap-2" + ? "fixed flex flex-col items-center w-20 h-screen gap-4 p-4 duration-150 bg-gray-100 dark:bg-slate-700 z-50 xs:p-2 xs:w-15 xs:gap-2" : "fixed flex flex-col w-72 h-screen gap-4 p-4 bg-gray-50 dark:bg-slate-700 z-40 duration-150" } > diff --git a/frontend/src/components/ReviewCard/ReviewCard.tsx b/frontend/src/components/ReviewCard/ReviewCard.tsx index 8cd966f7d..5b40c653d 100644 --- a/frontend/src/components/ReviewCard/ReviewCard.tsx +++ b/frontend/src/components/ReviewCard/ReviewCard.tsx @@ -88,7 +88,7 @@ export default function ReviewCard({ }; return ( -
+
{/* Title + Date */}

{review.title ? review.title : "-"}

diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index ee037cf01..83718bcf9 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -25,7 +25,7 @@ module.exports = { "unilectives-subheadings": "#989898", "unilectives-placeholder": "#606060", "unilectives-tags": "#CCEBF6", - "unilectives-card": "#FAFAFA", + "unilectives-card": "#f6f6f6", "unilectives-light-blue": "#84CEE7", "unilectives-purple": "#B789E5", "unilectives-indigo": "#9BADE8",