From 57a286082528088a23a58c38935a43caad47c5fe Mon Sep 17 00:00:00 2001 From: Jacob Sommer Date: Sat, 7 Dec 2024 21:10:51 -0800 Subject: [PATCH 01/11] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa17b36a..04bb2fab 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ git clone https://github.com//peterportal-client 4. Run `pnpm install` to install all node dependencies for the site and API. This may take a few minutes. -5. Rename the `.env.example` file in the api directory to `.env`. This includes the minimum environment variables needed for running the backend. +5. Make a copy of the `.env.example` file in the api directory and name it `.env`. This includes the minimum environment variables needed for running the backend. 6. (Optional) Set up your own PostgreSQL database and Google OAuth to be able to test features that require signing in such as leaving reviews or saving roadmaps to your account. Add additional variables/secrets to the .env file from the previous step. From f3a09d872993a4d6bf9b24b8edd7b54b000078a1 Mon Sep 17 00:00:00 2001 From: Jacob Sommer Date: Sun, 8 Dec 2024 14:31:52 -0800 Subject: [PATCH 02/11] remove review updated_at default --- api/drizzle/0001_heavy_xavin.sql | 1 + api/drizzle/meta/0001_snapshot.json | 634 ++++++++++++++++++++++++++++ api/drizzle/meta/_journal.json | 7 + api/src/db/schema.ts | 2 +- 4 files changed, 643 insertions(+), 1 deletion(-) create mode 100644 api/drizzle/0001_heavy_xavin.sql create mode 100644 api/drizzle/meta/0001_snapshot.json diff --git a/api/drizzle/0001_heavy_xavin.sql b/api/drizzle/0001_heavy_xavin.sql new file mode 100644 index 00000000..b08b743a --- /dev/null +++ b/api/drizzle/0001_heavy_xavin.sql @@ -0,0 +1 @@ +ALTER TABLE "review" ALTER COLUMN "updated_at" DROP DEFAULT; \ No newline at end of file diff --git a/api/drizzle/meta/0001_snapshot.json b/api/drizzle/meta/0001_snapshot.json new file mode 100644 index 00000000..305c2084 --- /dev/null +++ b/api/drizzle/meta/0001_snapshot.json @@ -0,0 +1,634 @@ +{ + "id": "5cf771c6-fe23-4061-932b-81c6c0c7a8ae", + "prevId": "150643a6-771c-4cba-b1bc-b3cb060d15ba", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.planner": { + "name": "planner", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "planner_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "years": { + "name": "years", + "type": "jsonb[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "planners_user_id_idx": { + "name": "planners_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "planner_user_id_user_id_fk": { + "name": "planner_user_id_user_id_fk", + "tableFrom": "planner", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.report": { + "name": "report", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "report_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "review_id": { + "name": "review_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reports_review_id_idx": { + "name": "reports_review_id_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "report_review_id_review_id_fk": { + "name": "report_review_id_review_id_fk", + "tableFrom": "report", + "tableTo": "review", + "columnsFrom": ["review_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review": { + "name": "review", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "review_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "professor_id": { + "name": "professor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "anonymous": { + "name": "anonymous", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rating": { + "name": "rating", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grade_received": { + "name": "grade_received", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "for_credit": { + "name": "for_credit", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "quarter": { + "name": "quarter", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "take_again": { + "name": "take_again", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "textbook": { + "name": "textbook", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "attendance": { + "name": "attendance", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "reviews_professor_id_idx": { + "name": "reviews_professor_id_idx", + "columns": [ + { + "expression": "professor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reviews_course_id_idx": { + "name": "reviews_course_id_idx", + "columns": [ + { + "expression": "course_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_user_id_user_id_fk": { + "name": "review_user_id_user_id_fk", + "tableFrom": "review", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_review": { + "name": "unique_review", + "nullsNotDistinct": false, + "columns": ["user_id", "professor_id", "course_id"] + } + }, + "policies": {}, + "checkConstraints": { + "rating_check": { + "name": "rating_check", + "value": "\"review\".\"rating\" >= 1 AND \"review\".\"rating\" <= 5" + }, + "difficulty_check": { + "name": "difficulty_check", + "value": "\"review\".\"difficulty\" >= 1 AND \"review\".\"difficulty\" <= 5" + } + }, + "isRLSEnabled": false + }, + "public.saved_course": { + "name": "saved_course", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "course_id": { + "name": "course_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "saved_course_user_id_user_id_fk": { + "name": "saved_course_user_id_user_id_fk", + "tableFrom": "saved_course", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "saved_course_user_id_course_id_pk": { + "name": "saved_course_user_id_course_id_pk", + "columns": ["user_id", "course_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sid": { + "name": "sid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sess": { + "name": "sess", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "expire": { + "name": "expire", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.transferred_course": { + "name": "transferred_course", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "course_name": { + "name": "course_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "units": { + "name": "units", + "type": "real", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "transferred_courses_user_id_idx": { + "name": "transferred_courses_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transferred_course_user_id_user_id_fk": { + "name": "transferred_course_user_id_user_id_fk", + "tableFrom": "transferred_course", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "user_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "google_id": { + "name": "google_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "picture": { + "name": "picture", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_roadmap_edit_at": { + "name": "last_roadmap_edit_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_google_id": { + "name": "unique_google_id", + "nullsNotDistinct": false, + "columns": ["google_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vote": { + "name": "vote", + "schema": "", + "columns": { + "review_id": { + "name": "review_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "votes_user_id_idx": { + "name": "votes_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "vote_review_id_review_id_fk": { + "name": "vote_review_id_review_id_fk", + "tableFrom": "vote", + "tableTo": "review", + "columnsFrom": ["review_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "vote_user_id_user_id_fk": { + "name": "vote_user_id_user_id_fk", + "tableFrom": "vote", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "vote_review_id_user_id_pk": { + "name": "vote_review_id_user_id_pk", + "columns": ["review_id", "user_id"] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "votes_vote_check": { + "name": "votes_vote_check", + "value": "\"vote\".\"vote\" = 1 OR \"vote\".\"vote\" = -1" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/api/drizzle/meta/_journal.json b/api/drizzle/meta/_journal.json index 28471cbc..a8247973 100644 --- a/api/drizzle/meta/_journal.json +++ b/api/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1731572839686, "tag": "0000_third_nekra", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1733697058204, + "tag": "0001_heavy_xavin", + "breakpoints": true } ] } diff --git a/api/src/db/schema.ts b/api/src/db/schema.ts index f9fb1cd0..7166150d 100644 --- a/api/src/db/schema.ts +++ b/api/src/db/schema.ts @@ -56,7 +56,7 @@ export const review = pgTable( difficulty: integer('difficulty').notNull(), gradeReceived: text('grade_received').notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), - updatedAt: timestamp('updated_at').defaultNow(), + updatedAt: timestamp('updated_at'), forCredit: boolean('for_credit').notNull(), quarter: text('quarter').notNull(), takeAgain: boolean('take_again').notNull(), From fda540a98cf8b0d3943d7ab6d5f7e0f34aabb188 Mon Sep 17 00:00:00 2001 From: Jacob Sommer Date: Mon, 9 Dec 2024 14:25:46 -0800 Subject: [PATCH 03/11] fix: search page crash (#530) * Fix search page crash * Shorten gradeDistData non-empty condition --- site/src/component/GradeDist/GradeDist.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/site/src/component/GradeDist/GradeDist.tsx b/site/src/component/GradeDist/GradeDist.tsx index 70fcc82a..1d517f61 100644 --- a/site/src/component/GradeDist/GradeDist.tsx +++ b/site/src/component/GradeDist/GradeDist.tsx @@ -29,14 +29,14 @@ const GradeDist: FC = (props) => { * @param props attributes received from the parent element */ - const [gradeDistData, setGradeDistData] = useState(null!); + const [gradeDistData, setGradeDistData] = useState(); const [chartType, setChartType] = useState('bar'); const [currentQuarter, setCurrentQuarter] = useState(''); const [currentProf, setCurrentProf] = useState(''); - const [profEntries, setProfEntries] = useState(null!); + const [profEntries, setProfEntries] = useState(); const [currentCourse, setCurrentCourse] = useState(''); - const [courseEntries, setCourseEntries] = useState(null!); - const [quarterEntries, setQuarterEntries] = useState(null!); + const [courseEntries, setCourseEntries] = useState(); + const [quarterEntries, setQuarterEntries] = useState(); const fetchGradeDistData = useCallback(() => { let requests: Promise[]; @@ -74,7 +74,7 @@ const GradeDist: FC = (props) => { const professors: Set = new Set(); const result: Entry[] = []; - gradeDistData.forEach((match) => match.instructors.forEach((prof) => professors.add(prof))); + gradeDistData!.forEach((match) => match.instructors.forEach((prof) => professors.add(prof))); Array.from(professors) .sort((a, b) => a.localeCompare(b)) @@ -92,7 +92,7 @@ const GradeDist: FC = (props) => { const courses: Set = new Set(); const result: Entry[] = []; - gradeDistData.forEach((match) => courses.add(match.department + ' ' + match.courseNumber)); + gradeDistData!.forEach((match) => courses.add(match.department + ' ' + match.courseNumber)); Array.from(courses) .sort((a, b) => a.localeCompare(b)) @@ -104,7 +104,7 @@ const GradeDist: FC = (props) => { // update list of professors/courses when new course/professor is detected useEffect(() => { - if (gradeDistData && gradeDistData.length !== 0) { + if (gradeDistData?.length) { if (props.course) { createProfEntries(); } else if (props.professor) { @@ -121,7 +121,7 @@ const GradeDist: FC = (props) => { const quarters: Set = new Set(); const result: Entry[] = [{ value: 'ALL', text: 'All Quarters' }]; - gradeDistData + gradeDistData! .filter((entry) => { if (props.course && entry.instructors.includes(currentProf)) { return true; @@ -156,7 +156,7 @@ const GradeDist: FC = (props) => { // update list of quarters when new professor/course is chosen useEffect(() => { - if ((currentProf || currentCourse) && gradeDistData.length !== 0) { + if ((currentProf || currentCourse) && gradeDistData?.length) { createQuarterEntries(); } }, [currentProf, currentCourse, createQuarterEntries, gradeDistData]); @@ -230,7 +230,7 @@ const GradeDist: FC = (props) => { ); - if (gradeDistData !== null && gradeDistData.length !== 0) { + if (gradeDistData?.length) { const graphProps = { gradeData: gradeDistData, quarter: currentQuarter, From 68f5e96bed9f1a92fec051c00782eed4e2d5070d Mon Sep 17 00:00:00 2001 From: Jacob Sommer Date: Mon, 9 Dec 2024 14:31:06 -0800 Subject: [PATCH 04/11] Update PR template (#527) --- .github/pull_request_template.md | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8c8259d5..524add78 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -6,36 +6,14 @@ ## Screenshots - - + - - +## Test Plan -## Steps to verify/test this change: - -- [ ] Verify changes work as expected on staging instance - - -## Final Checks: - -- [ ] Verify successful deployment - -(optional) - -- [ ] Write tests -- [ ] Write documentation + ## Issues - + Closes # From b966cb593b4c562f3f3724c62de50f06982f3355 Mon Sep 17 00:00:00 2001 From: Jacob Sommer Date: Mon, 9 Dec 2024 14:37:00 -0800 Subject: [PATCH 05/11] refactor: split GitHub workflows (#528) * Split build and deploy workflow for prod and staging * Created actions for repeated steps * Fixed concurrency groups * Added condition to not deploy staging on fork --- .github/actions/setup-pnpm/action.yml | 27 ++++++++++ .github/workflows/build-and-deploy.yml | 74 -------------------------- .github/workflows/clean-up-pr.yml | 61 --------------------- .github/workflows/deploy-prod.yml | 46 ++++++++++++++++ .github/workflows/deploy-staging.yml | 50 +++++++++++++++++ .github/workflows/lint.yml | 28 ++-------- .github/workflows/remove-staging.yml | 41 ++++++++++++++ 7 files changed, 167 insertions(+), 160 deletions(-) create mode 100644 .github/actions/setup-pnpm/action.yml delete mode 100644 .github/workflows/build-and-deploy.yml delete mode 100644 .github/workflows/clean-up-pr.yml create mode 100644 .github/workflows/deploy-prod.yml create mode 100644 .github/workflows/deploy-staging.yml create mode 100644 .github/workflows/remove-staging.yml diff --git a/.github/actions/setup-pnpm/action.yml b/.github/actions/setup-pnpm/action.yml new file mode 100644 index 00000000..41137e7b --- /dev/null +++ b/.github/actions/setup-pnpm/action.yml @@ -0,0 +1,27 @@ +name: Setup pnpm +runs: + using: composite + steps: + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: pnpm/action-setup@v3 + name: Install pnpm + with: + version: 9 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - uses: actions/cache@v4 + name: Setup pnpm cache + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml deleted file mode 100644 index b8a11908..00000000 --- a/.github/workflows/build-and-deploy.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Build and deploy - -on: - push: - branches: - - main - pull_request: - types: - - opened - - reopened - - synchronize - -# do not cancel in progress, SST will be stuck in a "locked" state if cancelled mid-deployment -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - -jobs: - build_and_deploy: - name: Build and deploy PeterPortal - runs-on: ubuntu-latest - if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'no deploy')) - environment: - name: ${{ (github.event_name == 'pull_request' && format('staging-{0}', github.event.pull_request.number)) || 'production' }} - url: https://${{ (github.event_name == 'pull_request' && format('staging-{0}.', github.event.pull_request.number)) || '' }}peterportal.org - - steps: - - name: Check Out Repo - uses: actions/checkout@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - uses: pnpm/action-setup@v3 - name: Install pnpm - with: - version: 9 - run_install: false - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - uses: actions/cache@v4 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install Dependencies - run: pnpm install - env: - HUSKY: 0 - - - name: Build and deploy - run: pnpm sst deploy --stage ${{ (github.event_name == 'pull_request' && format('staging-{0}', github.event.pull_request.number)) || 'prod' }} - env: - CI: false - PUBLIC_API_URL: ${{secrets.PUBLIC_API_URL}} - DATABASE_URL: ${{ github.event_name == 'pull_request' && secrets.DEV_DATABASE_URL || secrets.PROD_DATABASE_URL }} - SESSION_SECRET: ${{secrets.SESSION_SECRET}} - GOOGLE_CLIENT: ${{secrets.GOOGLE_CLIENT}} - GOOGLE_SECRET: ${{secrets.GOOGLE_SECRET}} - GRECAPTCHA_SECRET: ${{secrets.GRECAPTCHA_SECRET}} - ADMIN_EMAILS: ${{secrets.ADMIN_EMAILS}} - PRODUCTION_DOMAIN: ${{secrets.PRODUCTION_DOMAIN}} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - NODE_ENV: ${{ github.event_name == 'pull_request' && 'staging' || 'production' }} - ANTEATER_API_KEY: ${{ secrets.ANTEATER_API_KEY }} diff --git a/.github/workflows/clean-up-pr.yml b/.github/workflows/clean-up-pr.yml deleted file mode 100644 index d25d107d..00000000 --- a/.github/workflows/clean-up-pr.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: Clean up PR - -on: - pull_request: - types: [closed] - -# use pr number for group instead of github.ref because ref will be main branch when the PR closes which is not a unique group for the PR -concurrency: - group: ${{ github.workflow }}-pr-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - clean-up-pr: - runs-on: ubuntu-latest - - steps: - - name: Check Out Repo - uses: actions/checkout@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - uses: pnpm/action-setup@v3 - name: Install pnpm - with: - version: 9 - run_install: false - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - uses: actions/cache@v4 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- - - - name: Install Dependencies - run: pnpm install - env: - HUSKY: 0 - - - name: Remove staging stack - run: pnpm sst remove --stage staging-${{ github.event.pull_request.number }} - env: - CI: false - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - - - name: Deactivate deployment - uses: strumwolf/delete-deployment-environment@v3.0.0 - with: - environment: staging-${{ github.event.pull_request.number }} - token: ${{ secrets.GITHUB_TOKEN }} - onlyDeactivateDeployments: true diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 00000000..a74d9617 --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,46 @@ +name: Deploy production + +on: + push: + branches: + - main + +# do not cancel in progress, SST will be stuck in a "locked" state if cancelled mid-deployment +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + +jobs: + build_and_deploy: + name: Build and deploy + runs-on: ubuntu-latest + environment: + name: production + url: https://peterportal.org + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm + + - name: Install dependencies + run: pnpm install + env: + HUSKY: 0 + + - name: Build and deploy + run: pnpm sst deploy --stage prod + env: + DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }} + NODE_ENV: production + + PUBLIC_API_URL: ${{ secrets.PUBLIC_API_URL }} + SESSION_SECRET: ${{ secrets.SESSION_SECRET }} + GOOGLE_CLIENT: ${{ secrets.GOOGLE_CLIENT }} + GOOGLE_SECRET: ${{ secrets.GOOGLE_SECRET }} + GRECAPTCHA_SECRET: ${{ secrets.GRECAPTCHA_SECRET }} + ADMIN_EMAILS: ${{ secrets.ADMIN_EMAILS }} + PRODUCTION_DOMAIN: ${{ secrets.PRODUCTION_DOMAIN }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + ANTEATER_API_KEY: ${{ secrets.ANTEATER_API_KEY }} diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 00000000..0edc4a30 --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,50 @@ +name: Deploy staging + +on: + pull_request: + types: + - opened + - reopened + - synchronize + +# do not cancel in progress, SST will be stuck in a "locked" state if cancelled mid-deployment +concurrency: + group: staging-${{ github.event.pull_request.number }} + +jobs: + build_and_deploy: + name: Build and deploy + runs-on: ubuntu-latest + # don't run if labeled "no deploy" && don't run on PRs from forks + if: (!contains(github.event.pull_request.labels.*.name, 'no deploy')) && github.event.pull_request.head.repo.full_name == github.repository + environment: + name: staging-${{ github.event.pull_request.number }} + url: https://staging-${{ github.event.pull_request.number }}.peterportal.org + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm + + - name: Install dependencies + run: pnpm install + env: + HUSKY: 0 + + - name: Build and deploy + run: pnpm sst deploy --stage staging-${{ github.event.pull_request.number }} + env: + DATABASE_URL: ${{ secrets.DEV_DATABASE_URL }} + NODE_ENV: staging + + PUBLIC_API_URL: ${{ secrets.PUBLIC_API_URL }} + SESSION_SECRET: ${{ secrets.SESSION_SECRET }} + GOOGLE_CLIENT: ${{ secrets.GOOGLE_CLIENT }} + GOOGLE_SECRET: ${{ secrets.GOOGLE_SECRET }} + GRECAPTCHA_SECRET: ${{ secrets.GRECAPTCHA_SECRET }} + ADMIN_EMAILS: ${{ secrets.ADMIN_EMAILS }} + PRODUCTION_DOMAIN: ${{ secrets.PRODUCTION_DOMAIN }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + ANTEATER_API_KEY: ${{ secrets.ANTEATER_API_KEY }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ea0aec03..b35febac 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,34 +15,12 @@ jobs: lint: name: Lint and check formatting runs-on: ubuntu-latest - steps: - - name: Check Out Repo + - name: Checkout repo uses: actions/checkout@v4 - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - uses: pnpm/action-setup@v3 - name: Install pnpm - with: - version: 9 - run_install: false - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - uses: actions/cache@v4 - name: Setup pnpm cache - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store- + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm - name: Install Dependencies run: pnpm install diff --git a/.github/workflows/remove-staging.yml b/.github/workflows/remove-staging.yml new file mode 100644 index 00000000..0165c1cd --- /dev/null +++ b/.github/workflows/remove-staging.yml @@ -0,0 +1,41 @@ +name: Remove staging + +on: + pull_request: + types: + - closed + +# use pr number for group instead of github.ref because ref will be main branch when the PR closes which is not a unique group for the PR +# group should match with deploy-staging workflow so those don't run concurrently (if someone closes/reopens a PR) +concurrency: + group: staging-${{ github.event.pull_request.number }} + +jobs: + clean-up-pr: + runs-on: ubuntu-latest + # don't run on PRs from forks + if: github.event.pull_request.head.repo.full_name == github.repository + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm + + - name: Install Dependencies + run: pnpm install + env: + HUSKY: 0 + + - name: Remove staging + run: pnpm sst remove --stage staging-${{ github.event.pull_request.number }} + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + + - name: Deactivate deployment + uses: strumwolf/delete-deployment-environment@v3.0.0 + with: + environment: staging-${{ github.event.pull_request.number }} + token: ${{ secrets.GITHUB_TOKEN }} + onlyDeactivateDeployments: true From c4db8149d4efa4bc81323fac452d9fc0e02e5723 Mon Sep 17 00:00:00 2001 From: Jacob Sommer Date: Wed, 18 Dec 2024 10:01:05 -0800 Subject: [PATCH 06/11] refactor: use review slice on verify reviews page (#542) fix: vote color updates on verify reviews page --- site/src/component/Verify/Verify.tsx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/site/src/component/Verify/Verify.tsx b/site/src/component/Verify/Verify.tsx index 7a25ba46..ae6ed455 100644 --- a/site/src/component/Verify/Verify.tsx +++ b/site/src/component/Verify/Verify.tsx @@ -1,34 +1,36 @@ -import { FC, useEffect, useState } from 'react'; +import { FC, useCallback, useEffect, useState } from 'react'; import SubReview from '../../component/Review/SubReview'; import Button from 'react-bootstrap/Button'; import { Divider } from 'semantic-ui-react'; import './Verify.scss'; import trpc from '../../trpc'; -import { ReviewData } from '@peterportal/types'; +import { selectReviews, setReviews } from '../../store/slices/reviewSlice'; +import { useAppDispatch, useAppSelector } from '../../store/hooks'; const Verify: FC = () => { - const [reviews, setReviews] = useState([]); + const reviews = useAppSelector(selectReviews); const [loaded, setLoaded] = useState(false); + const dispatch = useAppDispatch(); - const getUnverifiedReviews = async () => { + const getUnverifiedReviews = useCallback(async () => { const res = await trpc.reviews.get.query({ verified: false }); - setReviews(res); + dispatch(setReviews(res)); setLoaded(true); - }; + }, [dispatch]); useEffect(() => { getUnverifiedReviews(); document.title = 'Verify Reviews | PeterPortal'; - }, []); + }, [getUnverifiedReviews]); const verifyReview = async (reviewId: number) => { await trpc.reviews.verify.mutate({ id: reviewId }); - setReviews(reviews.filter((review) => review.id !== reviewId)); + dispatch(setReviews(reviews.filter((review) => review.id !== reviewId))); }; const deleteReview = async (reviewId: number) => { await trpc.reviews.delete.mutate({ id: reviewId }); - setReviews(reviews.filter((review) => review.id !== reviewId)); + dispatch(setReviews(reviews.filter((review) => review.id !== reviewId))); }; if (!loaded) { From 159a57b2a2d4038d55d670316ecb95b58c9f706a Mon Sep 17 00:00:00 2001 From: Vincent Ho Date: Sun, 5 Jan 2025 19:51:57 -0800 Subject: [PATCH 07/11] Fix Professor Name Overflow (#544) * Name overflow change * Fixed formatting --------- Co-authored-by: Vincent Ho --- site/src/component/SearchPopup/SearchPopup.scss | 16 ++++++++++++++++ site/src/component/SearchPopup/SearchPopup.tsx | 13 +++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/site/src/component/SearchPopup/SearchPopup.scss b/site/src/component/SearchPopup/SearchPopup.scss index 148a8033..82027a52 100644 --- a/site/src/component/SearchPopup/SearchPopup.scss +++ b/site/src/component/SearchPopup/SearchPopup.scss @@ -110,6 +110,22 @@ color: var(--peterportal-mid-gray); } } +.search-popup-professor-name { + display: flex; + flex-wrap: wrap; +} + +.search-popup-professor-name > span { + display: inline-block; + margin-right: 0.3em; +} + +.search-popup-professor-name > span.ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; +} @media only screen and (max-width: 1300px) { .search-popup-infos { diff --git a/site/src/component/SearchPopup/SearchPopup.tsx b/site/src/component/SearchPopup/SearchPopup.tsx index c6e241b0..9bda698d 100644 --- a/site/src/component/SearchPopup/SearchPopup.tsx +++ b/site/src/component/SearchPopup/SearchPopup.tsx @@ -117,8 +117,17 @@ const SearchPopupContent: FC = (props) => { / 5.0 - - {score.name} + + + {score.name.split(' ').map((part, idx) => { + const isTooLong = part.length > 13; + return ( + + {part} + + ); + })} + ))} From 4462ac84a4baf24240ab9fa955266a6a5211b9be Mon Sep 17 00:00:00 2001 From: Jacob Sommer Date: Tue, 7 Jan 2025 18:14:49 -0800 Subject: [PATCH 08/11] Remove axios (#539) --- api/package.json | 1 - api/src/helpers/recaptcha.ts | 12 ++++++++---- pnpm-lock.yaml | 35 +++++++++++++++-------------------- site/package.json | 1 - 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/api/package.json b/api/package.json index 4b4aa4cd..12f71f1c 100644 --- a/api/package.json +++ b/api/package.json @@ -14,7 +14,6 @@ "dependencies": { "@trpc/server": "^10.45.2", "@vendia/serverless-express": "^4.12.6", - "axios": "^1.7.8", "connect-pg-simple": "^10.0.0", "cookie-parser": "^1.4.7", "dotenv-flow": "^4.1.0", diff --git a/api/src/helpers/recaptcha.ts b/api/src/helpers/recaptcha.ts index 61d40268..a2fcf4a8 100644 --- a/api/src/helpers/recaptcha.ts +++ b/api/src/helpers/recaptcha.ts @@ -1,5 +1,4 @@ import { ReviewSubmission } from '@peterportal/types'; -import axios from 'axios'; export async function verifyCaptcha(review: ReviewSubmission) { const reqBody = { @@ -7,9 +6,14 @@ export async function verifyCaptcha(review: ReviewSubmission) { response: review.captchaToken ?? '', }; const query = new URLSearchParams(reqBody); - const response = await axios - .post('https://www.google.com/recaptcha/api/siteverify?' + query) - .then((x) => x.data) + const response = await fetch('https://www.google.com/recaptcha/api/siteverify?' + query, { method: 'POST' }) + .then((res) => { + if (res.ok) { + return res.json(); + } else { + throw new Error(res.statusText); + } + }) .catch((e) => { console.error('Error validating captcha response', e); return { success: false }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2fde7963..535334f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,9 +54,6 @@ importers: '@vendia/serverless-express': specifier: ^4.12.6 version: 4.12.6 - axios: - specifier: ^1.7.8 - version: 1.7.9 connect-pg-simple: specifier: ^10.0.0 version: 10.0.0 @@ -163,9 +160,6 @@ importers: '@trpc/client': specifier: ^10.45.2 version: 10.45.2(@trpc/server@10.45.2) - axios: - specifier: ^1.7.8 - version: 1.7.9 bootstrap: specifier: ^4.6.2 version: 4.6.2(jquery@3.7.1)(popper.js@1.16.1) @@ -3960,9 +3954,6 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rfdc@1.3.1: - resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==} - rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -6642,7 +6633,8 @@ snapshots: ast-types-flow@0.0.8: {} - asynckit@0.4.0: {} + asynckit@0.4.0: + optional: true available-typed-arrays@1.0.7: dependencies: @@ -6675,6 +6667,7 @@ snapshots: proxy-from-env: 1.1.0 transitivePeerDependencies: - debug + optional: true axobject-query@4.1.0: {} @@ -6846,6 +6839,7 @@ snapshots: combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 + optional: true commander@12.1.0: {} @@ -7035,7 +7029,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - delayed-stream@1.0.0: {} + delayed-stream@1.0.0: + optional: true depd@2.0.0: {} @@ -7659,7 +7654,8 @@ snapshots: flatted@3.3.1: {} - follow-redirects@1.15.6: {} + follow-redirects@1.15.6: + optional: true for-each@0.3.3: dependencies: @@ -7670,6 +7666,7 @@ snapshots: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 + optional: true forwarded@0.2.0: {} @@ -8195,7 +8192,7 @@ snapshots: mqtt-packet@6.10.0: dependencies: bl: 4.1.0 - debug: 4.3.4 + debug: 4.3.7 process-nextick-args: 2.0.1 transitivePeerDependencies: - supports-color @@ -8205,7 +8202,7 @@ snapshots: dependencies: commist: 1.1.0 concat-stream: 2.0.0 - debug: 4.3.4 + debug: 4.3.7 duplexify: 4.1.3 help-me: 3.0.0 inherits: 2.0.4 @@ -8216,7 +8213,7 @@ snapshots: pump: 3.0.0 readable-stream: 3.6.2 reinterval: 1.1.0 - rfdc: 1.3.1 + rfdc: 1.4.1 split2: 3.2.2 ws: 7.5.9 xtend: 4.0.2 @@ -8279,7 +8276,7 @@ snapshots: number-allocator@1.0.14: dependencies: - debug: 4.3.4 + debug: 4.3.7 js-sdsl: 4.3.0 transitivePeerDependencies: - supports-color @@ -8564,7 +8561,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-from-env@1.1.0: {} + proxy-from-env@1.1.0: + optional: true pstree.remy@1.1.8: {} @@ -8848,9 +8846,6 @@ snapshots: reusify@1.0.4: {} - rfdc@1.3.1: - optional: true - rfdc@1.4.1: {} rimraf@3.0.2: diff --git a/site/package.json b/site/package.json index ad60ac9b..b2125fcf 100644 --- a/site/package.json +++ b/site/package.json @@ -9,7 +9,6 @@ "@nivo/pie": "^0.88.0", "@reduxjs/toolkit": "^2.4.0", "@trpc/client": "^10.45.2", - "axios": "^1.7.8", "bootstrap": "^4.6.2", "node-html-parser": "^6.1.13", "react": "^18.3.1", From aecd5d1747221d068690efbbb3cb1f46cce77e21 Mon Sep 17 00:00:00 2001 From: Timofey Obraztsov <35554964+timobraz@users.noreply.github.com> Date: Sat, 11 Jan 2025 13:07:24 -0800 Subject: [PATCH 09/11] added button to duplicate roadmap (#532) * added button to duplicate roadmap * fix: improved duplicate panel to be more like AA * fix: brought back copy index --- .../pages/RoadmapPage/RoadmapMultiplan.tsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/site/src/pages/RoadmapPage/RoadmapMultiplan.tsx b/site/src/pages/RoadmapPage/RoadmapMultiplan.tsx index 362c1431..026a09b2 100644 --- a/site/src/pages/RoadmapPage/RoadmapMultiplan.tsx +++ b/site/src/pages/RoadmapPage/RoadmapMultiplan.tsx @@ -20,6 +20,7 @@ interface RoadmapSelectableItemProps { index: number; clickHandler: () => void; editHandler: () => void; + duplicateHandler: () => void; deleteHandler: () => void; } @@ -28,6 +29,7 @@ const RoadmapSelectableItem: FC = ({ index, clickHandler, editHandler, + duplicateHandler, deleteHandler, }) => { return ( @@ -38,6 +40,9 @@ const RoadmapSelectableItem: FC = ({ + @@ -89,6 +94,24 @@ const RoadmapMultiplan: FC = () => { setEditIdx(-1); }; + const duplicatePlan = (plan: RoadmapPlan) => { + let newName = `${plan.name} (Copy)`; + let counter = 1; + while (allPlans.plans.find((p) => p.name === newName)) { + counter++; + newName = `${plan.name} (Copy ${counter})`; + } + dispatch( + addRoadmapPlan({ + name: newName, + content: JSON.parse(JSON.stringify(plan.content)), + }), + ); + const newIndex = allPlans.plans.length; + setCurrentPlanIndex(newIndex); + dispatch(setPlanIndex(newIndex)); + }; + useEffect(() => { document.title = `${name} | PeterPortal`; }, [name]); @@ -121,6 +144,7 @@ const RoadmapMultiplan: FC = () => { setCurrentPlanIndex(index); }} editHandler={() => setEditIdx(index)} + duplicateHandler={() => duplicatePlan(plan)} deleteHandler={() => setDelIdx(index)} /> ))} From 90898f89b21ed30a3d74710b7dd8f251a060a63a Mon Sep 17 00:00:00 2001 From: Awesome-E <54484616+Awesome-E@users.noreply.github.com> Date: Mon, 13 Jan 2025 09:46:54 -0800 Subject: [PATCH 10/11] Move Feedback Form to Asana (#546) * Change Feedback Form to Asana * Remove Missing professor form - this would be an API issue, not something that we should hard-code anyways --- site/src/component/AppHeader/AppHeader.tsx | 2 +- site/src/component/Footer/Footer.tsx | 6 +++++- site/src/component/ReviewForm/ReviewForm.tsx | 5 ----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/site/src/component/AppHeader/AppHeader.tsx b/site/src/component/AppHeader/AppHeader.tsx index 7ea472a0..82203bef 100644 --- a/site/src/component/AppHeader/AppHeader.tsx +++ b/site/src/component/AppHeader/AppHeader.tsx @@ -85,7 +85,7 @@ const AppHeader: FC = () => { diff --git a/site/src/component/Footer/Footer.tsx b/site/src/component/Footer/Footer.tsx index bd13eda6..1519e242 100644 --- a/site/src/component/Footer/Footer.tsx +++ b/site/src/component/Footer/Footer.tsx @@ -12,7 +12,11 @@ const Footer: FC = () => { API - + Feedback diff --git a/site/src/component/ReviewForm/ReviewForm.tsx b/site/src/component/ReviewForm/ReviewForm.tsx index f28fc98d..dd7ea156 100644 --- a/site/src/component/ReviewForm/ReviewForm.tsx +++ b/site/src/component/ReviewForm/ReviewForm.tsx @@ -197,11 +197,6 @@ const ReviewForm: FC = ({ ); })} - - - Can't find your professor? - - Missing instructor ); From 48a52180571ac110944cc3a59ebe2e40172eb95a Mon Sep 17 00:00:00 2001 From: Timofey Obraztsov <35554964+timobraz@users.noreply.github.com> Date: Mon, 13 Jan 2025 15:29:09 -0800 Subject: [PATCH 11/11] fix: transfer courses now counted for prereqs in search (#531) --- .../component/SearchHitContainer/SearchHitContainer.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/site/src/component/SearchHitContainer/SearchHitContainer.tsx b/site/src/component/SearchHitContainer/SearchHitContainer.tsx index 4e705722..31c51a48 100644 --- a/site/src/component/SearchHitContainer/SearchHitContainer.tsx +++ b/site/src/component/SearchHitContainer/SearchHitContainer.tsx @@ -29,10 +29,16 @@ const SearchResults = ({ quarter.courses.map((course) => course.department + ' ' + course.courseNumber), ), ); + const transfers = roadmap?.transfers.map((transfer) => transfer.name); if (index === 'courses') { return (results as CourseGQLData[]).map((course, i) => { const requiredCourses = Array.from( - validateCourse(new Set(allExistingCourses), course.prerequisiteTree, new Set(), course.corequisites), + validateCourse( + new Set([...allExistingCourses, ...transfers]), + course.prerequisiteTree, + new Set(), + course.corequisites, + ), ); return ( 0 && { requiredCourses })} />