Skip to content

Conversation

@AJ-505
Copy link
Collaborator

@AJ-505 AJ-505 commented Oct 25, 2025

Summary by CodeRabbit

  • New Features

    • Added visual role badge on dashboard sidebar showing Tutor or Student.
    • Added Signals section in Settings with tutor-only quick access and a loading skeleton.
  • Bug Fixes

    • Fixed role detection to reliably identify tutors from profile data.
  • Improvements

    • Onboarding now resets state automatically on page load.
    • Signals section shows an error state if loading fails.

@vercel
Copy link

vercel bot commented Oct 25, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
tutorlink Ready Ready Preview Comment Oct 25, 2025 0:32am

@coderabbitai
Copy link

coderabbitai bot commented Oct 25, 2025

Walkthrough

Runtime role fetching was added and consumed across UI and server: the sidebar shows a dynamic Tutor/Student badge, the settings page conditionally renders a Signals block with loading/error states for tutors, onboarding store gained a reset() called on mount, and the signal router's getViewerRole now queries the DB using ctx.user.id instead of always returning STUDENT.

Changes

Cohort / File(s) Summary
Dashboard Role-Based UI
src/app/dashboard/_components/sidebar.tsx, src/app/dashboard/settings/page.tsx
Add TRPC role query (api.signal.getViewerRole.useQuery()), derive isTutor, show dynamic badge in sidebar (🎓 Tutor / 👤 Student), add loading/error UI and tutor-only Signals block with link to /dashboard/signal.
Onboarding State Reset
src/app/onboarding/page.tsx, src/stores/onboarding.ts
Add and export reset() in onboarding store; call reset() once on mount via useEffect in onboarding page to clear onboarding state.
Signal Router Endpoint
src/server/api/routers/signal.ts
Update getViewerRole to accept ({ ctx }), query DB for user by clerkUid === ctx.user.id and return user?.role ?? "STUDENT". Update role checks (e.g., in acceptSignal) to look up user by clerkUid for authorization.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Browser UI
  participant TRPC as TRPC API (signalRouter)
  participant DB as Database

  rect rgb(230,240,255)
    UI->>TRPC: api.signal.getViewerRole.query()
    TRPC->>DB: findUnique({ where: { clerkUid: ctx.user.id } })
    DB-->>TRPC: user (role or null)
    alt user.role exists
      TRPC-->>UI: { role: "TUTOR" }
    else no user
      TRPC-->>UI: { role: "STUDENT" }
    end
    UI->>UI: render badge (🎓 Tutor / 👤 Student) and conditional Signals UI
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review DB lookup and clause (lookup by clerkUid: ctx.user.id) for correctness and indexing.
  • Verify tRPC procedure signature change (async ({ ctx })) and usage across callers.
  • Check UI branches: loading, error, tutor-only rendering and routes (/dashboard/signal).
  • Inspect onboarding reset() side effects and ensure no unintended data loss.

Possibly related PRs

  • Signal feature #22 — Adds or modifies signalRouter procedures; closely related to the updated getViewerRole and router changes.
  • Signals returned list #26 — Edits server router logic for signal-related DB queries and identifier usage; overlaps with this PR's router edits.
  • Feat: Migrate to Clerk for Auth #21 — Refactors Clerk auth/context and user-id handling; related to changes using ctx.user.id and role lookups.

Suggested reviewers

  • Tobshub

Poem

🐰 I hop, I query, I check who's true,
A badge for tutors, a student too,
Onboard reset at first light,
Signals show for tutors right,
Hooray — code hops clean and new! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The pull request title "Feat/dashboard wip" is vague and lacks meaningful specificity about the actual changes. While the title does reference "dashboard," which is relevant to the changeset (most modifications are in the dashboard directory), the term "wip" (work in progress) is a non-descriptive placeholder that fails to convey what feature is actually being implemented. A teammate scanning the commit history would not understand the primary change from this title alone; they only know it's dashboard-related work in progress. The changes involve implementing role-based UI features, tutor-specific dashboard components, and backend role fetching, but none of this is evident from the title. Revise the pull request title to be more descriptive and specific about the feature being implemented. A better title would clearly indicate the main change, such as "Add role-based dashboard UI and tutor signals" or "Implement viewer role detection with role-based components." This would provide clarity about the actual functionality being added rather than using placeholder terms like "wip."
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/dashboard-wip

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: 3

🧹 Nitpick comments (1)
src/app/dashboard/_components/sidebar.tsx (1)

26-27: Consider lifting the role query to a common parent component.

Both the sidebar (this file) and the settings page (src/app/dashboard/settings/page.tsx) independently fetch the viewer role using api.signal.getViewerRole.useQuery(). While tRPC/react-query will cache the result, this creates redundant query definitions and could be simplified.

Consider creating a shared hook or lifting the query to a common parent in the dashboard layout to avoid duplication and ensure consistent loading states across components.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8edfe11 and c27d9ee.

📒 Files selected for processing (4)
  • src/app/dashboard/_components/sidebar.tsx (2 hunks)
  • src/app/dashboard/settings/page.tsx (1 hunks)
  • src/app/onboarding/page.tsx (1 hunks)
  • src/server/api/routers/signal.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/server/api/routers/signal.ts (1)
src/server/api/trpc.ts (1)
  • protectedProcedure (104-104)
src/app/dashboard/_components/sidebar.tsx (1)
src/trpc/react.tsx (1)
  • api (25-25)
src/app/dashboard/settings/page.tsx (1)
src/trpc/react.tsx (1)
  • api (25-25)
src/app/onboarding/page.tsx (1)
src/stores/onboarding.ts (1)
  • useOnboardingStore (96-220)
🔇 Additional comments (1)
src/app/onboarding/page.tsx (1)

12-15: Verify the reset-on-mount behavior aligns with user experience goals.

The useEffect hook clears all onboarding data every time the component mounts. If a user navigates away (e.g., using the browser back button) and returns to this page, their role selection and all progress will be lost.

Consider whether this is the intended behavior, or if you want to preserve selections during a session. If reset is only needed for new sessions, you might want to add a condition or move the reset logic elsewhere.

Comment on lines +26 to +27
const { data: role } = api.signal.getViewerRole.useQuery();
const isTutor = role === "TUTOR";
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add loading and error states for the role query.

Similar to the settings page, the role query lacks loading and error handling. While loading, role will be undefined, causing isTutor to be false and displaying "👤 Student" as the default. This could confuse tutors who see the wrong badge briefly before it updates.

Add loading and error states:

-    const { data: role } = api.signal.getViewerRole.useQuery();
+    const { data: role, isLoading } = api.signal.getViewerRole.useQuery();
     const isTutor = role === "TUTOR";

Then conditionally render the badge:

     <div className="px-3 py-4">
         <div className="text-lg font-semibold text-neutral-900">TutorLink</div>
-        <div className="mt-2 inline-block px-2.5 py-1 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-900">
-            {isTutor ? "🎓 Tutor" : "👤 Student"}
-        </div>
+        {!isLoading && (
+            <div className="mt-2 inline-block px-2.5 py-1 rounded-full text-xs font-semibold bg-indigo-100 text-indigo-900">
+                {isTutor ? "🎓 Tutor" : "👤 Student"}
+            </div>
+        )}
     </div>

Committable suggestion skipped: line range outside the PR's diff.

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: 1

Caution

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

⚠️ Outside diff range comments (1)
src/server/api/routers/signal.ts (1)

109-141: Critical: Inconsistent handling of studentId across endpoints.

The acceptSignal endpoint assumes signal.studentId is an internal database ID (line 137: { id: signal.studentId }), but createSignal (line 29) sets studentId: ctx.user.id directly without querying the database first.

Given that lines 113-114 query by clerkUid: ctx.user.id, this confirms ctx.user.id is the Clerk UID (external identifier), not the internal database ID. This creates a mismatch:

  • Line 29 stores the Clerk UID in signal.studentId
  • Line 137 tries to connect a user by { id: signal.studentId }, expecting an internal ID

This will cause the Prisma connect operation to fail because it cannot find a user with id = <clerkUid>.

Solution: Update createSignal to first resolve the internal database ID:

 createSignal: protectedProcedure
     .input(z.object({
         message: z.string().min(1),
         subject: z.string().min(1),
         urgency: z.number().min(1).max(5),
         status: z.string()
     }))
     .mutation(async ({ input, ctx }) => {
-        const studentId = ctx.user.id;
+        // Resolve internal DB ID from Clerk UID
+        const student = await ctx.db.user.findUnique({
+            where: { clerkUid: ctx.user.id },
+            select: { id: true },
+        });
+        if (!student) {
+            throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
+        }
+        const studentId = student.id;
         const signalData = { ...input, studentId, createdAt: new Date().toISOString() };

Note: This same issue affects getMySignals (line 87) and deleteSignal (line 99), which also query/check studentId using ctx.user.id directly. Apply the same fix pattern to those endpoints.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c27d9ee and 3eb26c8.

📒 Files selected for processing (2)
  • src/app/dashboard/settings/page.tsx (1 hunks)
  • src/server/api/routers/signal.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/dashboard/settings/page.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/server/api/routers/signal.ts (1)
src/server/api/trpc.ts (1)
  • protectedProcedure (104-104)

Comment on lines +10 to +18
getViewerRole: protectedProcedure.query(async ({ ctx }) => {
// Query the DB user to get the canonical role
const user = await ctx.db.user.findUnique({
where: { clerkUid: ctx.user.id },
select: { role: true },
});

// Return the user's role from DB or default to STUDENT if not found
return user?.role ?? "STUDENT";
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

Good fix for the role consistency issue.

This change standardizes role determination by querying the canonical user.role field, resolving the previous inconsistency with acceptSignal. The fallback to "STUDENT" provides safe default behavior.

However, verify whether ctx.user already contains the role information to avoid an unnecessary DB query on every call:

If the role is already available in ctx.user, consider using it directly instead of querying the database.


🏁 Script executed:

#!/bin/bash
# Check if ctx.user is populated with role during authentication
rg -nP --type=ts -C5 'enforceUserIsAuthed|ctx\.user\s*='

Length of output: 907





🏁 Script executed:

#!/bin/bash
# Find where ctx is created/typed in TRPC setup
rg -nP --type=ts -B5 -A10 'export.*context|type.*Context|createContext'

Length of output: 4650


🏁 Script executed:

#!/bin/bash
# Find Signal model schema and studentId field type
fd -e prisma | head -5

Length of output: 20


🏁 Script executed:

#!/bin/bash
# Check createSignal implementation (line 29) and acceptSignal (lines 112-137)
rg -nP --type=ts -A30 'createSignal.*protectedProcedure\.input'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if studentId is used elsewhere and how it's referenced
rg -nP --type=ts 'studentId' -B2 -A2

Length of output: 5492



---

🏁 Script executed:

```shell
#!/bin/bash
# Check Signal model schema in prisma
rg -A20 'model Signal' prisma/schema.prisma

Length of output: 536


🏁 Script executed:

#!/bin/bash
# Check User model to confirm id vs clerkUid fields
rg -A10 'model User' prisma/schema.prisma

Length of output: 404


getViewerRole fix is correct, but critical bug found in createSignal that must be fixed.

The DB query in getViewerRole is necessary and correct—ctx.user (from Clerk) lacks the role field, which exists only in the database. However, verification reveals a critical issue:

createSignal (line 29) stores ctx.user.id as studentId. Since ctx.user.id is the Clerk UID (not the internal DB User.id), this causes a data integrity failure: acceptSignal (line 137) attempts to connect the conversation participant using { id: signal.studentId }, which fails because it passes a Clerk UID to a field expecting an internal database ID.

Fix required: createSignal must query the database to get the internal User.id before storing it in studentId, similar to how acceptSignal queries for me.id at line 124.

🤖 Prompt for AI Agents
In src/server/api/routers/signal.ts around lines 10-18 (and specifically
createSignal at ~line 29), createSignal currently writes ctx.user.id (Clerk UID)
into studentId which is the internal DB User.id; change createSignal to query
ctx.db.user.findUnique({ where: { clerkUid: ctx.user.id }, select: { id: true }
}) and use the returned internal id as studentId before creating the signal,
handling the case where the DB user is not found (throw or return an error)
similar to the me lookup used in acceptSignal around line 124 so subsequent
acceptSignal participant connects work correctly.

@AJ-505 AJ-505 merged commit 7382d70 into main Oct 25, 2025
3 checks passed
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.

2 participants