Skip to content

[FOSSOVERFLOW-25] Fixed an issue where the search bar kept re-rendering on each input. #56

Open
alSN0W wants to merge 36 commits intoOpenLake:mainfrom
alSN0W:searchbar
Open

[FOSSOVERFLOW-25] Fixed an issue where the search bar kept re-rendering on each input. #56
alSN0W wants to merge 36 commits intoOpenLake:mainfrom
alSN0W:searchbar

Conversation

@alSN0W
Copy link

@alSN0W alSN0W commented Mar 9, 2026

Bug Fixes & Feature Enhancement: Search Filter Improvements

Summary

Fixes critical search input focus issue and implements real-time search with debouncing for better user experience.


Fixes

Search Input Losing Focus After Each Keystroke

Problem: Users had to re-click the search input after typing each character.

Root Cause: Parent component re-renders triggered useEffect to reset localSearchQuery while user was actively typing, causing the input to lose focus.

Solution:

  • Change both from arrow function components to plain JSX variables.

Enhancements

Real-Time Search with Debouncing

Previous: Users had to click "Apply" button after typing search query.

New: Search executes automatically as users type with 300ms debounce delay.

Implementation:

  • Search triggers after user stops typing (300ms delay)
  • Local state updates instantly for immediate visual feedback
  • Proper cleanup of timeouts on component unmount

Benefits:

  • Instant user feedback
  • Reduced API load
  • Matches industry standard UX (Google, Amazon)
  • No manual "Apply" needed for search

Testing

  • Search input maintains focus while typing
  • Debounce triggers after 300ms of inactivity
  • "Clear All" clears search immediately
  • No memory leaks from timeout cleanup
  • Other filters still use "Apply" button as intended

Breaking Changes

None. Backward compatible enhancement.

Summary by CodeRabbit

Release Notes

  • New Features

    • Enabled magic link email authentication for sign-in
    • Added helpful/unhelpful voting on course reviews with vote counting
    • Implemented pagination controls for reviewing course feedback
    • Added debounced search filtering for improved responsiveness
  • Updates

    • Upgraded to React 19 and Next.js 16 for enhanced performance
    • Refactored review display with integrated voting interface

alSN0W and others added 30 commits February 9, 2026 23:30
fixed displaying the user votes incorrectly issue
Styled Voting buttons and updated migration files
removed useVote as it's useless for now
added pagination type and utils
Revert "added pagination type and utils"
made changes recommended by code-rabbit
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
removed all the useless routes and functions
@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Walkthrough

This PR introduces pagination and voting system refactoring alongside React/Next.js dependency upgrades to major versions (React 19, Next 16). It adds pagination infrastructure (utilities, hooks, components), refactors the voting/rating API endpoints, introduces new UI components (Pagination, VoteButton), updates database schema and RLS policies, enables the magic link signin form, and modifies the JSX runtime configuration.

Changes

Cohort / File(s) Summary
Dependencies & Configuration
package.json, next.config.js, tsconfig.json, next-env.d.ts
Upgrades React and Next.js to major versions (React 19, Next 16); adds turbopack config; updates JSX runtime to react-jsx; adds Next.js types route path.
Type Definitions
src/types/pagination.ts, src/types/reviews.tsx
Introduces pagination interfaces (PaginationParams, PaginationMeta, PaginatedResponse). Restructures Review type with target_id and target_type, removes course-specific fields, adds Vote and VoteType types, introduces ReviewWithUserVote and vote-related response types.
Pagination Infrastructure
src/lib/pagination.ts, src/lib/withPagination.ts, src/hooks/usePaginatedReviews.ts
Creates pagination utilities (calculatePagination, validatePaginationParams, getOffset, buildPaginationResponse), a higher-order withPagination helper for API routes, and a usePaginatedReviews hook for client-side paginated review fetching.
UI Components
src/components/common/Pagination.tsx, src/components/common/VoteButton.tsx
Adds new Pagination component with page navigation and ellipsis rendering; introduces VoteButton component for upvote/downvote with optimistic UI, authentication checks, and server synchronization.
Course Page & Filters
src/app/courses/[courseId]/page.tsx, src/components/courses/Filters.tsx, src/components/courses/course_page/CoursePageReviews.tsx
Updates CoursePage to use Promise-wrapped params with React use hook; refactors Filters with debounced search and JSX variable components; replaces CoursePageReviews with pagination-based rendering, per-review voting UI, and anonymous name generation.
Authentication
src/app/auth/signin/page.tsx
Enables previously commented-out Magic link form UI and submission handling.
API Routes
src/pages/api/ratings/route.ts, src/pages/api/ratings/vote/route.ts, src/pages/api/ratings/vote/index.ts, src/pages/api/reviews/index.ts
Removes old ratings and vote POST/DELETE handlers; introduces new /api/ratings/vote/index.ts with CRUD-like vote operations (POST/GET/DELETE); creates /api/reviews/index.ts with pagination-wrapped review fetching.
Supabase Utilities
src/utils/supabase/server.ts, src/utils/supabase/server-pages.ts
Awaits cookies() call in server.ts; adds new createClient helper for SSR client construction in server-pages.ts with cookie handling.
Database Migration
src/migrations/migration.sql
Adds auth_id column and unique constraint to votes table; updates vote and flag policies to role-based access control; modifies update_review_votes function with SECURITY DEFINER and search_path; reworks handle_new_user logic with existence checks; removes on_auth_user_created trigger; updates indexes and policy names.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Client as Client Component
    participant API as /api/ratings/vote
    participant DB as Supabase (votes table)
    participant Auth as Supabase Auth

    User->>Client: Click Upvote/Downvote Button
    Client->>Client: Optimistic UI Update
    Client->>Auth: Get User Session
    alt Unauthenticated
        Auth-->>Client: No session
        Client->>Client: Show toast & rollback
    else Authenticated
        Auth-->>Client: Session with auth.uid()
        Client->>API: POST {review_id, vote_type}
        API->>DB: Check existing vote by auth_id
        alt Vote exists & same type
            DB-->>API: Found matching vote
            API->>DB: Delete vote
            DB-->>API: Success
            API-->>Client: action: removed
        else Vote exists & different type
            DB-->>API: Found different vote
            API->>DB: Update vote_type
            DB-->>API: Success
            API-->>Client: action: updated
        else No existing vote
            DB-->>API: Not found
            API->>DB: Insert new vote
            DB-->>API: Success
            API-->>Client: action: created
        end
        Client->>Client: Update UI with response
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • fix review #39: Modifies CoursePageReviews.tsx to update anonymous name generation and display logic, overlapping with the review rendering changes in this PR.

Poem

🐰 Hop-hop! Reviews now vote with grace,
Pagination guides the reader's pace,
React and Next leap forward bright,
With voting buttons, all feels right!
The votes flow swift through RLS gates,
While we celebrate our modernized fates.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title directly addresses the main fix (search bar re-rendering issue), though it focuses on a specific symptom rather than the comprehensive changes (pagination, voting, reviews refactor). Consider whether the title should reflect the broader scope of changes (pagination system, voting mechanism, review refactoring) beyond just the search bar fix, or narrow the PR scope.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
package.json (2)

60-61: ⚠️ Potential issue | 🟠 Major

Update eslint-config-next to match Next.js version.

eslint-config-next at 13.5.1 is significantly behind Next.js 16.x. This may cause linting rule mismatches and false positives/negatives for Next.js 16 features.

Proposed fix
     "eslint": "8.49.0",
-    "eslint-config-next": "13.5.1",
+    "eslint-config-next": "^16.1.6",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 60 - 61, Update the eslint-config-next dependency
to a version compatible with Next.js 16 to avoid lint rule mismatches: open
package.json and change the "eslint-config-next" entry to match the Next.js
major version (e.g., upgrade to a 16.x-compatible release), then run your
package manager (npm/yarn/pnpm) to install and verify linting; ensure the "next"
dependency version and "eslint-config-next" major versions align.

19-19: ⚠️ Potential issue | 🔴 Critical

Critical: @next/swc-wasm-nodejs and eslint-config-next must be updated to match Next.js 16.

Both @next/swc-wasm-nodejs (line 19) and eslint-config-next (line 61) are pinned at 13.5.1 while next is ^16.1.6. The SWC and ESLint packages must match the Next.js major version to avoid build and configuration errors.

Update both to:

  • @next/swc-wasm-nodejs: 16.1.6
  • eslint-config-next: 16.1.6

Alternatively, remove @next/swc-wasm-nodejs entirely if you're not explicitly using WASM-based SWC (standard Next.js 16 installations don't require it).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 19, Update the pinned packages in package.json so they
match the Next.js 16 major version: change "@next/swc-wasm-nodejs" from "13.5.1"
to "16.1.6" (or remove it entirely if you don't need the WASM SWC) and change
"eslint-config-next" from "13.5.1" to "16.1.6"; ensure package.json reflects
these new versions and run install (npm/yarn) to update lockfile and verify
builds.
src/components/courses/Filters.tsx (1)

103-109: ⚠️ Potential issue | 🟠 Major

Parent search updates now wipe un-applied local filters.

This effect rehydrates departments, difficulties, and rating on every currentFilters change. Since search now updates the parent after debounce, typing in the search box will reset any local department/difficulty/rating edits the user has not applied yet.

Suggested split
-  useEffect(() => {
-    setLocalSearchQuery(currentFilters.searchQuery);
-    setLocalSelectedDepartments(currentFilters.departments);
-    setLocalSelectedDifficulties(currentFilters.difficulties);
-    setLocalRatingFilter([currentFilters.rating]);
-  }, [currentFilters]);
+  useEffect(() => {
+    setLocalSearchQuery(currentFilters.searchQuery);
+  }, [currentFilters.searchQuery]);
+
+  useEffect(() => {
+    setLocalSelectedDepartments(currentFilters.departments);
+    setLocalSelectedDifficulties(currentFilters.difficulties);
+    setLocalRatingFilter([currentFilters.rating]);
+  }, [
+    currentFilters.departments,
+    currentFilters.difficulties,
+    currentFilters.rating,
+  ]);
🧹 Nitpick comments (6)
src/utils/supabase/server-pages.ts (1)

2-4: Remove unused imports.

SerializeOptions (line 2) and serialize (line 4) from the cookie package are imported but never used. The code uses serializeCookieHeader from @supabase/ssr instead.

Proposed fix
 import { createServerClient, serializeCookieHeader } from '@supabase/ssr'
-import { SerializeOptions } from 'cookie'
 import { NextApiRequest, NextApiResponse } from 'next'
-import { serialize } from 'cookie'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/supabase/server-pages.ts` around lines 2 - 4, Remove the unused
imports from the cookie package: delete the SerializeOptions and serialize
imports at the top of the module since the code uses serializeCookieHeader from
`@supabase/ssr` instead; update any import statement referencing SerializeOptions
or serialize to eliminate them so only needed symbols (e.g.,
serializeCookieHeader) remain imported.
src/types/reviews.tsx (1)

29-35: Consider adding auth_id to Vote interface or documenting its omission.

The database schema (per migration.sql) includes auth_id in the votes table, and the API route writes it. The Vote interface omits this field.

If this is intentional (e.g., auth_id is server-only and shouldn't be exposed to clients), consider adding a comment to document this decision. Otherwise, add the field for schema consistency.

📝 Option 1: Document the omission
 /**
  * Vote from database - matches the votes table schema
+ * Note: auth_id is stored in the database but not exposed to clients
  */
 export interface Vote {
   id: string;
   review_id: string;
   anonymous_id: string;
   vote_type: 'helpful' | 'unhelpful';
   created_at: string;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types/reviews.tsx` around lines 29 - 35, The Vote interface currently
omits the auth_id column present in the database and written by the API route;
update the Vote interface (named Vote in src/types/reviews.tsx) to include
auth_id?: string if it should be exposed to clients, or add a concise comment
above the Vote interface explaining that auth_id exists in migration.sql and is
intentionally omitted from the client-facing type because it is server-only;
make sure the change references the same symbol Vote and the migration.sql/votes
table for clarity.
src/pages/api/ratings/vote/index.ts (2)

81-86: Updating created_at loses original vote timestamp.

When a user changes their vote type, the code overwrites created_at with the current time. This loses the information about when the user originally voted. Consider either:

  1. Keeping created_at unchanged (remove line 84), or
  2. Adding an updated_at field to track modifications while preserving the original creation time.
♻️ Proposed fix
         const { error: updateError } = await supabase
           .from('votes')
           .update({ 
             vote_type, 
             anonymous_id, // Update anonymous_id in case it changed
-            created_at: new Date().toISOString() 
           })
           .eq('id', existingVote.id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/api/ratings/vote/index.ts` around lines 81 - 86, The update call
that sets created_at currently overwrites the original vote timestamp—locate the
.update({...}) call that includes vote_type, anonymous_id, created_at and
existingVote.id and remove the created_at assignment so the original creation
time is preserved; if you need to track when the vote was changed instead, add
an updated_at field in the update payload (e.g., updated_at: new
Date().toISOString()) and ensure the database schema/migration and any model
handling recognize updated_at rather than overwriting created_at.

148-148: Consider limiting the number of review IDs to prevent unbounded queries.

The endpoint accepts any number of comma-separated review_ids without validation. A malicious or buggy client could send thousands of IDs, causing database performance issues.

♻️ Proposed fix
       const auth_id = user.id;
       const reviewIdArray = review_ids.split(',').map(id => id.trim());
+      
+      // Limit batch size to prevent unbounded queries
+      const MAX_REVIEW_IDS = 100;
+      if (reviewIdArray.length > MAX_REVIEW_IDS) {
+        return res.status(400).json({ 
+          error: `Maximum ${MAX_REVIEW_IDS} review_ids allowed per request` 
+        });
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/api/ratings/vote/index.ts` at line 148, The handler currently
builds reviewIdArray from review_ids without any limits; add input validation to
prevent unbounded queries by defining a max (e.g. const MAX_REVIEW_IDS = 50),
split/trim/filter/unique the review_ids string, validate each id format (numeric
or UUID depending on your model), and if the resulting array length is 0 or >
MAX_REVIEW_IDS return a 400 error; update the code around the line where
reviewIdArray is created (the review_ids variable and reviewIdArray mapping) to
enforce these checks before calling the database.
src/components/courses/course_page/CoursePageReviews.tsx (2)

15-15: Replace any with proper type for review.

Using any loses type safety. Import and use the Review type from the types file.

♻️ Proposed fix
+import { Review, VoteType } from "@/types/reviews";
+
 /* Single Review Card (vertical format) */
-const CourseReviewItem = ({ review, userVote }: { review: any; userVote?: VoteType }) => {
+const CourseReviewItem = ({ review, userVote }: { review: Review; userVote?: VoteType }) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/courses/course_page/CoursePageReviews.tsx` at line 15, The
CourseReviewItem prop uses review: any; update it to a concrete type by
importing the Review interface and using review: Review in the CourseReviewItem
props. Add an import for Review from the central types file (alongside the
existing VoteType import if needed), then replace the anonymous any in the
component signature "CourseReviewItem = ({ review, userVote }: { review: any;
userVote?: VoteType })" with the typed form "review: Review" so the component
and any consumers get proper type checking.

5-7: Import VoteType from the canonical types file.

VoteType is defined in both @/components/common/VoteButton and @/types/reviews.tsx. Import from the shared types file to maintain a single source of truth and avoid type drift.

♻️ Proposed fix
-import { VoteButton, VoteType } from "@/components/common/VoteButton";
+import { VoteButton } from "@/components/common/VoteButton";
+import { VoteType } from "@/types/reviews";
 import { Pagination } from "@/components/common/Pagination";
 import { usePaginatedReviews } from "@/hooks/usePaginatedReviews";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/courses/course_page/CoursePageReviews.tsx` around lines 5 - 7,
CoursePageReviews currently imports VoteType from the VoteButton module which
duplicates the canonical type; update the import in CoursePageReviews to import
VoteType from the shared reviews types module (the canonical types file) and
retain only the VoteButton import from the VoteButton module so the component
uses the single source of truth for VoteType; if VoteButton still exports
VoteType, remove or stop using that duplicate export to prevent type drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@next-env.d.ts`:
- Around line 4-7: The auto-generated import of "./.next/dev/types/routes.d.ts"
in next-env.d.ts should either be removed or enabled via Next.js typed routes;
either delete that import line from next-env.d.ts if you don't need typed
routes, or enable typed routes by adding typedRoutes: true to your
next.config.js (do not use experimental.typedRoutes). After enabling, rebuild so
Next.js regenerates this file automatically.

In `@src/components/common/VoteButton.tsx`:
- Around line 60-71: The handleVote function allows concurrent calls because
isLoading is set only after awaiting supabase.auth.getSession; introduce a
ref-based mutex (e.g., requestInFlightRef via useRef(false)) and check it at the
top of handleVote to return early if already true, set
requestInFlightRef.current = true and call setIsLoading(true) before any await,
then perform the supabase.auth.getSession call and existing optimistic
update/POST logic, and finally reset requestInFlightRef.current = false and
setIsLoading(false) in a finally block (being careful to clear the ref even when
returning early for an unauthenticated session) to avoid duplicate POSTs.

In `@src/components/courses/course_page/CoursePageReviews.tsx`:
- Around line 68-85: The page doesn't call the hook's refresh, so new reviews
only show after navigation; in CoursePageReviews destructure refresh from
usePaginatedReviews (alongside reviews, currentPage, etc.) and pass it as a
callback prop (e.g., onRefresh or refresh) into AddReviewButton so
AddReviewButton can invoke it after a successful submit to reload the current
page; update any AddReviewButton prop types/handler usage to call that callback
after submission.
- Around line 94-100: In CoursePageReviews, the fetch of
`/api/ratings/vote?review_ids=${reviewIds}` should check response.ok before
calling response.json(); update the async block around that fetch (the try block
that currently assigns response and votesData and calls setUserVotes) to: verify
response.ok, handle non-OK by parsing the error body or throwing a descriptive
error, and only call setUserVotes when votesData.success is true; include a
fallback to setUserVotes({}) or handle state accordingly and surface/log the
error so failures don’t silently break the component (refer to the variables
response, votesData, reviewIds, and the setUserVotes call).

In `@src/components/courses/Filters.tsx`:
- Around line 76-92: The debounced timeout in handleSearchChange closes over the
stale currentFilters snapshot, so when it fires it can overwrite newer filter
changes; fix by reading the latest filters at execution time instead of using
the closed-over currentFilters — introduce a ref (e.g., currentFiltersRef) that
mirror-updates whenever currentFilters changes and then inside the debounced
callback spread currentFiltersRef.current with searchQuery; keep
debouncedSearchRef and handleSearchChange intact but replace usage of
currentFilters in the timeout with currentFiltersRef.current to ensure the
merged FiltersState is always up-to-date when calling onFilterChange.
- Around line 69-70: The ref debouncedSearchRef is typed for NodeJS timers and
not initialized, which breaks React 19 client-side usage; change its type to a
browser-safe number and give it an initial value (e.g., useRef<number |
null>(null)) so debouncedSearchRef in the Filters component holds a numeric
timeout id (or null) instead of NodeJS.Timeout and satisfies useRef's required
initial value.

In `@src/hooks/usePaginatedReviews.ts`:
- Around line 4-12: Replace the locally declared Review interface in
usePaginatedReviews.ts with the canonical type import: remove the inline
"interface Review { ... }" and add "import { Review } from '@/types/reviews'";
ensure all usages in the file (e.g., any generics, state types, or function
signatures referencing Review) use the imported Review so the additional fields
like target_id, target_type, is_flagged, difficulty_rating, workload_rating,
knowledge_rating, teaching_rating, approachability_rating, and updated_at are
correctly typed.

In `@src/migrations/migration.sql`:
- Around line 457-459: The policy "Allow public to read votes" on the votes
table currently uses USING (true) which exposes sensitive columns (auth_id,
anonymous_id); change it so the raw votes table is not publicly selectable
(e.g., replace USING (true) with USING (false) or drop/disable the policy for
the votes table) and instead create a restricted read surface: add a view (e.g.,
reviews.votes or reviews.vote_totals) that only exposes aggregated
counts/allowed columns and grant SELECT on that view to public; ensure auth_id
and anonymous_id remain inaccessible from the public policy on the votes table.
- Around line 462-472: The RLS policies on the votes table only bind auth_id,
allowing clients to spoof anonymous_id; update the INSERT and UPDATE policies
for table votes (the "Authenticated users can insert their own votes" and "Users
can update their own votes" policies) so both identifiers are enforced: add
conditions that require anonymous_id to equal the caller's anonymous identifier
in both USING and WITH CHECK clauses (i.e., require auth_id = auth.uid() AND
anonymous_id = auth.anonymous_id(), or the equivalent function your auth system
exposes) so inserts/updates can only set both auth_id and anonymous_id to the
caller's values.

In `@src/pages/api/ratings/vote/index.ts`:
- Around line 46-56: The vote flow has a TOCTOU race: after checking for
existingVote you later call the insert (the supabase .from('votes').insert(...)
call) and treat all insert failures as generic 500s; catch the unique-constraint
duplicate key error from the insert (Postgres code '23505' or Supabase error
message containing "duplicate" / "unique") and handle it specially by returning
a 409 or treating it as a successful no-op (e.g., fetch and return the existing
vote) instead of a 500; update the insert error handling around the insert call
(the .insert(...) / .single() response handling) to detect error.code ===
'23505' (or check error.message) and respond accordingly.

In `@src/pages/api/reviews/index.ts`:
- Around line 78-83: The public reviews API is returning the stable field
anonymous_id in the .select(...) call which lets clients correlate anonymous
authors; remove anonymous_id from the returned columns (edit the .select call
that currently lists anonymous_id) so it is not sent in responses, keep using
anonymous_id internally for votes uniqueness if needed but never expose it, and
verify the votes(...) usage and any count logic still work after removing
anonymous_id from the selected projection.
- Around line 35-38: The current heuristic sets isUUID =
target_id.includes('-'), which misclassifies dashed codes as UUIDs; replace this
with a proper UUID validation (e.g., a dedicated isValidUUID function or a
library validator) and use that boolean in place of isUUID so only true RFC4122
UUIDs take the UUID branch; update the code that references target_id and isUUID
to call/assign isValidUUID(target_id) (or import/use validate from 'uuid') and
ensure the lookup path for non-UUID codes remains unchanged.

In `@src/types/reviews.tsx`:
- Around line 4-24: The Review interface must be updated to match the Supabase
ratings table: replace separated rating fields (rating_value, difficulty_rating,
workload_rating, knowledge_rating, teaching_rating, approachability_rating) with
a single rating_metrics object (containing keys like overall, difficulty,
workload, knowledge, teaching, approachability), rename votes to
helpfulness_score, and add semester, year, and display_date fields; then update
data access to query the actual "ratings" table (not "reviews") in
src/lib/data/reviews.tsx and use the correct foreign key (e.g., course_id if
present in the DB) and projection, and update
src/components/courses/CourseReviews.tsx to read ratings from
review.rating_metrics.overall (or appropriate nested keys) and avoid
non-existent review.user.* fields (map anonymous_id or the actual user structure
returned by the ratings row). Ensure the interface symbol Review and data access
in src/lib/data/reviews.tsx and CourseReviews.tsx are consistent with
rating_metrics and helpfulness_score names.

---

Outside diff comments:
In `@package.json`:
- Around line 60-61: Update the eslint-config-next dependency to a version
compatible with Next.js 16 to avoid lint rule mismatches: open package.json and
change the "eslint-config-next" entry to match the Next.js major version (e.g.,
upgrade to a 16.x-compatible release), then run your package manager
(npm/yarn/pnpm) to install and verify linting; ensure the "next" dependency
version and "eslint-config-next" major versions align.
- Line 19: Update the pinned packages in package.json so they match the Next.js
16 major version: change "@next/swc-wasm-nodejs" from "13.5.1" to "16.1.6" (or
remove it entirely if you don't need the WASM SWC) and change
"eslint-config-next" from "13.5.1" to "16.1.6"; ensure package.json reflects
these new versions and run install (npm/yarn) to update lockfile and verify
builds.

---

Nitpick comments:
In `@src/components/courses/course_page/CoursePageReviews.tsx`:
- Line 15: The CourseReviewItem prop uses review: any; update it to a concrete
type by importing the Review interface and using review: Review in the
CourseReviewItem props. Add an import for Review from the central types file
(alongside the existing VoteType import if needed), then replace the anonymous
any in the component signature "CourseReviewItem = ({ review, userVote }: {
review: any; userVote?: VoteType })" with the typed form "review: Review" so the
component and any consumers get proper type checking.
- Around line 5-7: CoursePageReviews currently imports VoteType from the
VoteButton module which duplicates the canonical type; update the import in
CoursePageReviews to import VoteType from the shared reviews types module (the
canonical types file) and retain only the VoteButton import from the VoteButton
module so the component uses the single source of truth for VoteType; if
VoteButton still exports VoteType, remove or stop using that duplicate export to
prevent type drift.

In `@src/pages/api/ratings/vote/index.ts`:
- Around line 81-86: The update call that sets created_at currently overwrites
the original vote timestamp—locate the .update({...}) call that includes
vote_type, anonymous_id, created_at and existingVote.id and remove the
created_at assignment so the original creation time is preserved; if you need to
track when the vote was changed instead, add an updated_at field in the update
payload (e.g., updated_at: new Date().toISOString()) and ensure the database
schema/migration and any model handling recognize updated_at rather than
overwriting created_at.
- Line 148: The handler currently builds reviewIdArray from review_ids without
any limits; add input validation to prevent unbounded queries by defining a max
(e.g. const MAX_REVIEW_IDS = 50), split/trim/filter/unique the review_ids
string, validate each id format (numeric or UUID depending on your model), and
if the resulting array length is 0 or > MAX_REVIEW_IDS return a 400 error;
update the code around the line where reviewIdArray is created (the review_ids
variable and reviewIdArray mapping) to enforce these checks before calling the
database.

In `@src/types/reviews.tsx`:
- Around line 29-35: The Vote interface currently omits the auth_id column
present in the database and written by the API route; update the Vote interface
(named Vote in src/types/reviews.tsx) to include auth_id?: string if it should
be exposed to clients, or add a concise comment above the Vote interface
explaining that auth_id exists in migration.sql and is intentionally omitted
from the client-facing type because it is server-only; make sure the change
references the same symbol Vote and the migration.sql/votes table for clarity.

In `@src/utils/supabase/server-pages.ts`:
- Around line 2-4: Remove the unused imports from the cookie package: delete the
SerializeOptions and serialize imports at the top of the module since the code
uses serializeCookieHeader from `@supabase/ssr` instead; update any import
statement referencing SerializeOptions or serialize to eliminate them so only
needed symbols (e.g., serializeCookieHeader) remain imported.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 68e08eaf-2f50-493c-b089-324c77869e8f

📥 Commits

Reviewing files that changed from the base of the PR and between 7f13bc0 and 3deea90.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (22)
  • next-env.d.ts
  • next.config.js
  • package.json
  • src/app/auth/signin/page.tsx
  • src/app/courses/[courseId]/page.tsx
  • src/components/common/Pagination.tsx
  • src/components/common/VoteButton.tsx
  • src/components/courses/Filters.tsx
  • src/components/courses/course_page/CoursePageReviews.tsx
  • src/hooks/usePaginatedReviews.ts
  • src/lib/pagination.ts
  • src/lib/withPagination.ts
  • src/migrations/migration.sql
  • src/pages/api/ratings/route.ts
  • src/pages/api/ratings/vote/index.ts
  • src/pages/api/ratings/vote/route.ts
  • src/pages/api/reviews/index.ts
  • src/types/pagination.ts
  • src/types/reviews.tsx
  • src/utils/supabase/server-pages.ts
  • src/utils/supabase/server.ts
  • tsconfig.json
💤 Files with no reviewable changes (2)
  • src/pages/api/ratings/vote/route.ts
  • src/pages/api/ratings/route.ts

Comment on lines +4 to +7
import "./.next/dev/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Next.js 16 typed routes configuration

💡 Result:

To enable typed routes (statically typed links) in Next.js 16, turn on the stable typedRoutes flag in your next.config.* (don’t use experimental.typedRoutes anymore). [1]

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  typedRoutes: true,
}

export default nextConfig

Requirements / what you get:

  • Your project must be using TypeScript. [1]
  • On next dev / next build, Next.js generates route/link type definitions under .next/types, which TypeScript uses to type-check Link href (and related navigation usage). [2]

Optionally, you can use the Route type from next:

import type { Route } from 'next'

const href: Route = '/dashboard'

[2]

Sources: [1] [2]


🏁 Script executed:

# Check the Next.js version in package.json
fd package.json -x cat {} | grep -A 2 -B 2 "next"

Repository: OpenLake/RateMyCourse

Length of output: 745


🏁 Script executed:

# Find and read the next-env.d.ts file
fd next-env.d.ts -x cat -n {}

Repository: OpenLake/RateMyCourse

Length of output: 430


🏁 Script executed:

# Search for how Next.js 16 generates and structures the typed routes file
# Let's look for any .next directory structure hints or related configuration
fd -t d ".next" -x echo {} 2>/dev/null | head -20

Repository: OpenLake/RateMyCourse

Length of output: 47


🏁 Script executed:

# Search the codebase for any references to the .next directory, next.config files, or typedRoutes configuration
rg -i "typedroutes|typed.*routes" -A 2 -B 2 --type ts --type js --type json

Repository: OpenLake/RateMyCourse

Length of output: 47


🏁 Script executed:

# Check if next.config file exists and its content
fd "next.config" -x cat -n {}

Repository: OpenLake/RateMyCourse

Length of output: 573


Remove or enable the typed routes import based on project needs.

This file is auto-generated by Next.js and should not be manually edited. Line 4 imports from ./.next/dev/types/routes.d.ts, which will only be generated if you enable typed routes. Either remove this import if typed routes are not needed, or enable them in next.config.js with typedRoutes: true (not experimental.typedRoutes, which is no longer used in Next.js 16). Once enabled, Next.js will regenerate this file automatically on each build.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@next-env.d.ts` around lines 4 - 7, The auto-generated import of
"./.next/dev/types/routes.d.ts" in next-env.d.ts should either be removed or
enabled via Next.js typed routes; either delete that import line from
next-env.d.ts if you don't need typed routes, or enable typed routes by adding
typedRoutes: true to your next.config.js (do not use experimental.typedRoutes).
After enabling, rebuild so Next.js regenerates this file automatically.

Comment on lines +60 to +71
const handleVote = async (voteType: 'helpful' | 'unhelpful') => {
if (isLoading) return;

// Check if user is authenticated BEFORE making API call
const { data: { session } } = await supabase.auth.getSession();

if (!session) {
toast.error('Please sign in to vote on reviews');
return;
}

setIsLoading(true);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Set the in-flight guard before the async session lookup.

isLoading does not flip until after Line 64 resolves, so two quick clicks can enter handleVote twice and send duplicate POSTs before the buttons disable. A ref-based mutex is the safest fix here.

Suggested shape
const requestInFlightRef = useRef(false);

const handleVote = async (voteType: 'helpful' | 'unhelpful') => {
  if (requestInFlightRef.current) return;
  requestInFlightRef.current = true;
  setIsLoading(true);

  try {
    const { data: { session } } = await supabase.auth.getSession();
    if (!session) {
      toast.error('Please sign in to vote on reviews');
      return;
    }

    // existing optimistic update + fetch...
  } finally {
    requestInFlightRef.current = false;
    setIsLoading(false);
  }
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/VoteButton.tsx` around lines 60 - 71, The handleVote
function allows concurrent calls because isLoading is set only after awaiting
supabase.auth.getSession; introduce a ref-based mutex (e.g., requestInFlightRef
via useRef(false)) and check it at the top of handleVote to return early if
already true, set requestInFlightRef.current = true and call setIsLoading(true)
before any await, then perform the supabase.auth.getSession call and existing
optimistic update/POST logic, and finally reset requestInFlightRef.current =
false and setIsLoading(false) in a finally block (being careful to clear the ref
even when returning early for an unauthenticated session) to avoid duplicate
POSTs.

Comment on lines +68 to +85
const {
reviews,
currentPage,
totalPages,
totalItems,
isLoading,
error,
hasNextPage,
hasPreviousPage,
goToPage,
} = usePaginatedReviews({
targetId: id,
targetType: 'course',
initialPage: 1,
limit: 10,
sortBy: 'created_at',
sortOrder: 'desc',
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

New reviews won't appear until page navigation.

The usePaginatedReviews hook provides a refresh() function (see line 123-126 of usePaginatedReviews.ts), but it's not destructured or passed to AddReviewButton. After submitting a new review, users won't see it until they navigate to another page and back.

Consider passing a refresh callback to AddReviewButton to reload the current page after submission.

🔧 Proposed fix
   const {
     reviews,
     currentPage,
     totalPages,
     totalItems,
     isLoading,
     error,
     hasNextPage,
     hasPreviousPage,
     goToPage,
+    refresh,
   } = usePaginatedReviews({
-        <AddReviewButton courseId={id} />
+        <AddReviewButton courseId={id} onSuccess={refresh} />

Also applies to: 116-116

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/courses/course_page/CoursePageReviews.tsx` around lines 68 -
85, The page doesn't call the hook's refresh, so new reviews only show after
navigation; in CoursePageReviews destructure refresh from usePaginatedReviews
(alongside reviews, currentPage, etc.) and pass it as a callback prop (e.g.,
onRefresh or refresh) into AddReviewButton so AddReviewButton can invoke it
after a successful submit to reload the current page; update any AddReviewButton
prop types/handler usage to call that callback after submission.

Comment on lines +94 to +100
try {
const response = await fetch(`/api/ratings/vote?review_ids=${reviewIds}`);
const votesData = await response.json();

if (votesData.success) {
setUserVotes(votesData.votes || {});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing error handling for non-OK response.

The fetch call doesn't check response.ok before parsing JSON. If the API returns an error status (4xx/5xx), response.json() may fail or return an error object that doesn't have success: true.

🛡️ Proposed fix
       try {
         const response = await fetch(`/api/ratings/vote?review_ids=${reviewIds}`);
+        if (!response.ok) {
+          console.error("Failed to fetch votes:", response.status);
+          return;
+        }
         const votesData = await response.json();
         
         if (votesData.success) {
           setUserVotes(votesData.votes || {});
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const response = await fetch(`/api/ratings/vote?review_ids=${reviewIds}`);
const votesData = await response.json();
if (votesData.success) {
setUserVotes(votesData.votes || {});
}
try {
const response = await fetch(`/api/ratings/vote?review_ids=${reviewIds}`);
if (!response.ok) {
console.error("Failed to fetch votes:", response.status);
return;
}
const votesData = await response.json();
if (votesData.success) {
setUserVotes(votesData.votes || {});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/courses/course_page/CoursePageReviews.tsx` around lines 94 -
100, In CoursePageReviews, the fetch of
`/api/ratings/vote?review_ids=${reviewIds}` should check response.ok before
calling response.json(); update the async block around that fetch (the try block
that currently assigns response and votesData and calls setUserVotes) to: verify
response.ok, handle non-OK by parsing the error body or throwing a descriptive
error, and only call setUserVotes when votesData.success is true; include a
fallback to setUserVotes({}) or handle state accordingly and surface/log the
error so failures don’t silently break the component (refer to the variables
response, votesData, reviewIds, and the setUserVotes call).

Comment on lines +69 to +70
// Ref to store the debounce timeout for search
const debouncedSearchRef = useRef<NodeJS.Timeout>();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In React 19 TypeScript, does useRefrequire an initial value, and what type is recommended for storingsetTimeouthandles in browser code instead ofNodeJS.Timeout?

💡 Result:

  • useRef initial value (React 19 + TypeScript): Yes—under React 19’s TypeScript typings you should pass an initial value (e.g. useRef<HTMLDivElement | null>(null) or useRef<number | undefined>(undefined)), rather than calling useRef() with no arguments. The hook is documented as useRef(initialValue). [1], [2]
  • setTimeout handle type in browser code: In browsers, setTimeout returns a number, so prefer number (or, to be explicit and avoid Node typings, ReturnType<typeof window.setTimeout>). [3], [4]

Sources
[1] React docs – useRef(initialValue) https://react.dev/reference/react/useRef
[2] React 19 migration note – “useRef() requires an initial value” https://deku.posstree.com/en/react/react-19-migration/
[3] Stack Overflow – browser setTimeout returns number, Node returns NodeJS.Timeout https://stackoverflow.com/questions/60245787/typescript-what-is-the-correct-type-for-a-timeout
[4] Stack Overflow – window.setTimeout typed as number vs Node typings https://stackoverflow.com/questions/45802988/typescript-use-correct-version-of-settimeout-node-vs-window


Initialize the debounce ref with a browser-safe type.

In React 19, useRef() requires an initial value. Additionally, NodeJS.Timeout is incorrect for client-side timers—setTimeout in browsers returns number.

Suggested fix
-  const debouncedSearchRef = useRef<NodeJS.Timeout>();
+  const debouncedSearchRef = useRef<number | null>(null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Ref to store the debounce timeout for search
const debouncedSearchRef = useRef<NodeJS.Timeout>();
// Ref to store the debounce timeout for search
const debouncedSearchRef = useRef<number | null>(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/courses/Filters.tsx` around lines 69 - 70, The ref
debouncedSearchRef is typed for NodeJS timers and not initialized, which breaks
React 19 client-side usage; change its type to a browser-safe number and give it
an initial value (e.g., useRef<number | null>(null)) so debouncedSearchRef in
the Filters component holds a numeric timeout id (or null) instead of
NodeJS.Timeout and satisfies useRef's required initial value.

Comment on lines +462 to +472
CREATE POLICY "Authenticated users can insert their own votes"
ON votes FOR INSERT
TO authenticated
WITH CHECK (auth_id = auth.uid());

-- Users can update their own votes
CREATE POLICY "Users can update their own votes"
ON votes FOR UPDATE
TO authenticated
USING (auth_id = auth.uid())
WITH CHECK (auth_id = auth.uid());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Vote write policies still allow anonymous ID spoofing.

These policies only bind auth_id. A client can still insert/update a vote with someone else’s anonymous_id, and because src/pages/api/reviews/index.ts:79 exposes that value today, they can occupy another user’s (review_id, anonymous_id) unique slot. The write checks should bind both identifiers to the caller.

Suggested fix
 CREATE POLICY "Authenticated users can insert their own votes" 
 ON votes FOR INSERT 
 TO authenticated
-WITH CHECK (auth_id = auth.uid());
+WITH CHECK (
+  auth_id = auth.uid()
+  AND anonymous_id = get_anonymous_id()
+);
@@
 CREATE POLICY "Users can update their own votes" 
 ON votes FOR UPDATE 
 TO authenticated
-USING (auth_id = auth.uid())
-WITH CHECK (auth_id = auth.uid());
+USING (
+  auth_id = auth.uid()
+  AND anonymous_id = get_anonymous_id()
+)
+WITH CHECK (
+  auth_id = auth.uid()
+  AND anonymous_id = get_anonymous_id()
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/migrations/migration.sql` around lines 462 - 472, The RLS policies on the
votes table only bind auth_id, allowing clients to spoof anonymous_id; update
the INSERT and UPDATE policies for table votes (the "Authenticated users can
insert their own votes" and "Users can update their own votes" policies) so both
identifiers are enforced: add conditions that require anonymous_id to equal the
caller's anonymous identifier in both USING and WITH CHECK clauses (i.e.,
require auth_id = auth.uid() AND anonymous_id = auth.anonymous_id(), or the
equivalent function your auth system exposes) so inserts/updates can only set
both auth_id and anonymous_id to the caller's values.

Comment on lines +46 to +56
const { data: existingVote, error: checkError } = await supabase
.from('votes')
.select('id, vote_type')
.eq('review_id', review_id)
.eq('auth_id', auth_id) // ← Check by real user, not anonymous_id
.single();

if (checkError && checkError.code !== 'PGRST116') {
console.error('Error checking existing vote:', checkError);
return res.status(500).json({ error: 'Failed to check existing vote' });
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential race condition between vote check and insert.

There's a TOCTOU (time-of-check to time-of-use) window between checking for an existing vote (lines 46-51) and inserting a new one (lines 101-108). Two concurrent requests could both pass the existence check and attempt to insert, causing a unique constraint violation on (review_id, auth_id).

The database constraint will catch this, but the error handling at line 110-113 treats all insert errors generically. Consider catching the unique constraint violation specifically and returning a more informative response (or treating it as a successful no-op).

🛡️ Proposed fix to handle duplicate constraint gracefully
       if (insertError) {
-        console.error('Error inserting vote:', insertError);
-        return res.status(500).json({ error: 'Failed to cast vote' });
+        // Handle unique constraint violation (concurrent request already inserted)
+        if (insertError.code === '23505') {
+          // Retry by fetching the existing vote
+          return res.status(409).json({ error: 'Vote already exists, please retry' });
+        }
+        console.error('Error inserting vote:', insertError);
+        return res.status(500).json({ error: 'Failed to cast vote' });
       }

Also applies to: 100-108

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/api/ratings/vote/index.ts` around lines 46 - 56, The vote flow has
a TOCTOU race: after checking for existingVote you later call the insert (the
supabase .from('votes').insert(...) call) and treat all insert failures as
generic 500s; catch the unique-constraint duplicate key error from the insert
(Postgres code '23505' or Supabase error message containing "duplicate" /
"unique") and handle it specially by returning a 409 or treating it as a
successful no-op (e.g., fetch and return the existing vote) instead of a 500;
update the insert error handling around the insert call (the .insert(...) /
.single() response handling) to detect error.code === '23505' (or check
error.message) and respond accordingly.

Comment on lines +35 to +38
// Check if target_id is a code (not a UUID)
// UUIDs have format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (contains dashes)
// Codes are simpler: mal100, cs101, etc. (no dashes)
const isUUID = target_id.includes('-');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use a real UUID check here.

Line 38 treats any identifier containing - as a UUID. A dashed course/professor identifier will skip the lookup path and be sent straight to the UUID column filter, which can produce empty results or malformed-UUID failures.

Suggested fix
-    const isUUID = target_id.includes('-');
+    const isUUID =
+      /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(target_id);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check if target_id is a code (not a UUID)
// UUIDs have format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (contains dashes)
// Codes are simpler: mal100, cs101, etc. (no dashes)
const isUUID = target_id.includes('-');
// Check if target_id is a code (not a UUID)
// UUIDs have format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (contains dashes)
// Codes are simpler: mal100, cs101, etc. (no dashes)
const isUUID =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(target_id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/api/reviews/index.ts` around lines 35 - 38, The current heuristic
sets isUUID = target_id.includes('-'), which misclassifies dashed codes as
UUIDs; replace this with a proper UUID validation (e.g., a dedicated isValidUUID
function or a library validator) and use that boolean in place of isUUID so only
true RFC4122 UUIDs take the UUID branch; update the code that references
target_id and isUUID to call/assign isValidUUID(target_id) (or import/use
validate from 'uuid') and ensure the lookup path for non-UUID codes remains
unchanged.

Comment on lines +78 to +83
.select(`
id, anonymous_id, rating_value, comment, votes,
is_flagged, difficulty_rating, workload_rating,
knowledge_rating, teaching_rating, approachability_rating,
created_at, updated_at
`, { count: 'exact' })
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t expose anonymous_id from a public reviews API.

This route is readable without authentication, so returning a stable anonymous_id lets clients correlate all reviews written by the same “anonymous” author across different targets. That weakens the platform’s anonymity model and also makes the votes(review_id, anonymous_id) uniqueness slot easier to abuse.

Suggested fix
       .select(`
-        id, anonymous_id, rating_value, comment, votes,
+        id, rating_value, comment, votes,
         is_flagged, difficulty_rating, workload_rating,
         knowledge_rating, teaching_rating, approachability_rating,
         created_at, updated_at
       `, { count: 'exact' })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.select(`
id, anonymous_id, rating_value, comment, votes,
is_flagged, difficulty_rating, workload_rating,
knowledge_rating, teaching_rating, approachability_rating,
created_at, updated_at
`, { count: 'exact' })
.select(`
id, rating_value, comment, votes,
is_flagged, difficulty_rating, workload_rating,
knowledge_rating, teaching_rating, approachability_rating,
created_at, updated_at
`, { count: 'exact' })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/api/reviews/index.ts` around lines 78 - 83, The public reviews API
is returning the stable field anonymous_id in the .select(...) call which lets
clients correlate anonymous authors; remove anonymous_id from the returned
columns (edit the .select call that currently lists anonymous_id) so it is not
sent in responses, keep using anonymous_id internally for votes uniqueness if
needed but never expose it, and verify the votes(...) usage and any count logic
still work after removing anonymous_id from the selected projection.

Comment on lines 4 to +24
export interface Review {
id: string;
courseId: string;
courseName: string;
courseCode: string;
semester: string;
overallRating: number;
workloadRating: number;
contentRating: number;
teachingRating: number;
supportRating: number;
comment: string;
date: string;
user: {
id: string;
name: string;
avatar: string;
};
anonymous_id: string;
target_id: string;
target_type: 'course' | 'professor';
rating_value: number; // Overall rating 1-5
comment: string | null;
votes: number; // Net helpful votes (helpful - unhelpful)
is_flagged: boolean;
created_at: string;
updated_at: string;

// Course-specific ratings (null for professor reviews)
difficulty_rating: number | null;
workload_rating: number | null;

// Professor-specific ratings (null for course reviews)
knowledge_rating: number | null;
teaching_rating: number | null;
approachability_rating: number | null;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find files still using old Review field names
echo "=== Files using old 'course_id' field ==="
rg -n "course_id" --type ts --type tsx -g '!**/migration*'

echo -e "\n=== Files using old 'overallRating' field ==="
rg -n "overallRating" --type ts --type tsx

echo -e "\n=== Files using 'review.user' pattern ==="
rg -n "review\.user\." --type ts --type tsx

Repository: OpenLake/RateMyCourse

Length of output: 297


🏁 Script executed:

#!/bin/bash
# Find files still using old Review field names
echo "=== Files using old 'course_id' field ==="
rg "course_id" -g "*.ts" -g "*.tsx"

echo -e "\n=== Files using old 'overallRating' field ==="
rg "overallRating" -g "*.ts" -g "*.tsx"

echo -e "\n=== Files using 'review.user' pattern ==="
rg "review\.user\." -g "*.ts" -g "*.tsx"

echo -e "\n=== Check Review interface in reviews.tsx ==="
cat src/types/reviews.tsx | head -30

Repository: OpenLake/RateMyCourse

Length of output: 4234


🏁 Script executed:

#!/bin/bash
# Check imports and usage in CourseReviews.tsx
echo "=== CourseReviews.tsx imports and Review type usage ==="
head -30 src/components/courses/CourseReviews.tsx
echo -e "\n=== Check variable type in CourseReviews ==="
rg -A 5 "const reviews" src/components/courses/CourseReviews.tsx | head -20

# Check imports and usage in lib/data/reviews.tsx
echo -e "\n=== lib/data/reviews.tsx content ==="
cat src/lib/data/reviews.tsx

Repository: OpenLake/RateMyCourse

Length of output: 1538


🏁 Script executed:

#!/bin/bash
# Check the Supabase types for Reviews table
echo "=== Supabase Reviews table schema ==="
rg -A 20 "type Tables = {" src/types/supabase.ts | rg -A 15 "reviews:"

echo -e "\n=== Check the complete Reviews definition ==="
rg -B 2 -A 15 "reviews:" src/types/supabase.ts | head -40

Repository: OpenLake/RateMyCourse

Length of output: 260


🏁 Script executed:

#!/bin/bash
# Read the supabase types file to find Reviews table definition
echo "=== Reading supabase.ts to find Reviews table ==="
cat src/types/supabase.ts | grep -A 30 "reviews"

Repository: OpenLake/RateMyCourse

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Check file size first
wc -l src/types/supabase.ts

# Read the entire supabase.ts file
cat src/types/supabase.ts

Repository: OpenLake/RateMyCourse

Length of output: 9619


Schema mismatch: Review interface does not match the actual Supabase ratings table.

The Review interface in src/types/reviews.tsx is incompatible with the database schema defined in src/types/supabase.ts:

  • Database has a ratings table (not reviews), with fields like rating_metrics (JSON object containing overall, difficulty, workload, etc.) instead of separate fields like rating_value and difficulty_rating
  • Database uses helpfulness_score instead of votes
  • Database includes semester, year, and display_date fields not in the interface
  • Code in src/lib/data/reviews.tsx queries a non-existent reviews table with a non-existent course_id field—this is already broken
  • src/components/courses/CourseReviews.tsx accesses review.overallRating and review.user.* which do not exist in either the interface or database schema

Align the Review interface with the actual ratings table schema and fix dependent queries and component accessors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types/reviews.tsx` around lines 4 - 24, The Review interface must be
updated to match the Supabase ratings table: replace separated rating fields
(rating_value, difficulty_rating, workload_rating, knowledge_rating,
teaching_rating, approachability_rating) with a single rating_metrics object
(containing keys like overall, difficulty, workload, knowledge, teaching,
approachability), rename votes to helpfulness_score, and add semester, year, and
display_date fields; then update data access to query the actual "ratings" table
(not "reviews") in src/lib/data/reviews.tsx and use the correct foreign key
(e.g., course_id if present in the DB) and projection, and update
src/components/courses/CourseReviews.tsx to read ratings from
review.rating_metrics.overall (or appropriate nested keys) and avoid
non-existent review.user.* fields (map anonymous_id or the actual user structure
returned by the ratings row). Ensure the interface symbol Review and data access
in src/lib/data/reviews.tsx and CourseReviews.tsx are consistent with
rating_metrics and helpfulness_score names.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant