feat: Builds an Alumni Mentorship Matching Module allowing alumni to opt-in as mentors with defined capacity limits and expertise tags - #3148
Conversation
📝 WalkthroughWalkthroughThe PR adds an alumni mentorship module with Supabase tables, capacity automation, access policies, a filterable mentor directory, profile cards, and a request submission modal. ChangesAlumni mentorship network
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Student
participant MentorDirectory
participant useMentorshipDirectory
participant Supabase
participant RequestMentorshipModal
participant mentorship_requests
Student->>MentorDirectory: Enter search or filter values
MentorDirectory->>useMentorshipDirectory: Update directory filters
useMentorshipDirectory->>Supabase: Query mentor profiles
Supabase-->>useMentorshipDirectory: Return filtered mentors
useMentorshipDirectory-->>MentorDirectory: Return directory state
Student->>MentorDirectory: Select an available mentor
MentorDirectory->>RequestMentorshipModal: Open selected mentor
Student->>RequestMentorshipModal: Submit introductory message
RequestMentorshipModal->>Supabase: Insert pending request
Supabase->>mentorship_requests: Store mentorship request
mentorship_requests-->>RequestMentorshipModal: Return submission result
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Biome (2.5.6)src/components/mentorship/MentorProfileCard.tsxFile contains syntax errors that prevent linting: Line 3: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 4: unterminated string literal src/components/mentorship/MentorDirectory.tsxFile contains syntax errors that prevent linting: Line 4: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 5: expected src/components/mentorship/RequestMentorshipModal.tsxFile contains syntax errors that prevent linting: Line 3: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 4: expected
🔧 ESLint
src/components/mentorship/MentorDirectory.tsxParsing error: ';' expected. src/components/mentorship/MentorProfileCard.tsxParsing error: ';' expected. src/components/mentorship/RequestMentorshipModal.tsxParsing error: ';' expected.
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 16
🧹 Nitpick comments (10)
src/hooks/useMentorshipDirectory.ts (2)
103-105: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReplace
err: anywith a narrowed error type.
catch (err: any)disables type checking on the error path. Useunknownand narrow before you readmessage.♻️ Proposed change
- } catch (err: any) { + } catch (err: unknown) { console.error('[useMentorshipDirectory] Fetch failed:', err); - setError(err.message || 'Failed to load mentor directory.'); + setError(err instanceof Error ? err.message : 'Failed to load mentor directory.');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/hooks/useMentorshipDirectory.ts` around lines 103 - 105, Update the catch clause in useMentorshipDirectory to use unknown instead of any, then narrow err before accessing message; preserve the existing fallback error text and console logging behavior.
62-63: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winDebounce the search query and cancel stale responses.
fetchMentorsdepends on the wholefiltersobject, andMentorDirectory.tsxLine 49 updatessearchon every keystroke. Each character sends a network request. Responses can also resolve out of order, so a slower earlier request can overwrite the newest result set.Debounce the search term and discard results from superseded requests with a cancellation flag or an
AbortController.Also applies to: 90-93
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/hooks/useMentorshipDirectory.ts` around lines 62 - 63, Update fetchMentors in useMentorshipDirectory to debounce requests triggered by search changes and cancel or ignore superseded requests, using an AbortController or cancellation flag so stale responses cannot update mentor results. Keep loading and result updates tied only to the latest active request, and clean up the debounce/cancellation state when dependencies change or the hook unmounts.src/components/mentorship/MentorProfileCard.tsx (2)
71-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExpose the capacity bar to assistive technology.
The progress bar is a pair of styled
divelements. Addrole="progressbar"witharia-valuenow,aria-valuemin, andaria-valuemaxso screen reader users receive the same capacity information that the visual bar conveys. The numeric text on Line 75 helps, but the bar itself carries the color state.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/mentorship/MentorProfileCard.tsx` around lines 71 - 85, Add progressbar accessibility attributes to the inner capacity-bar div in MentorProfileCard: set role="progressbar", aria-valuenow to the current capacity percentage, aria-valuemin to 0, and aria-valuemax to 100, while preserving the existing styling and width calculation.
88-98: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueExplain why the button is disabled.
A disabled button is removed from the tab order, so keyboard users cannot reach it to read the "Currently Full" label. Add
aria-disabledwith a visible explanation, or keep the label associated througharia-describedbyon the capacity text.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/mentorship/MentorProfileCard.tsx` around lines 88 - 98, Update the mentorship request button in MentorProfileCard to expose why it is unavailable to assistive technology while preserving the existing disabled behavior and “Currently Full” label. Add an accessible explanation using aria-disabled with descriptive text or associate the button with the capacity message via aria-describedby, ensuring the full-state reason remains perceivable to keyboard and screen-reader users.src/components/mentorship/MentorDirectory.tsx (1)
119-124: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAnnounce the loading and error states to assistive technology.
The skeleton grid and the error panel change silently. Add
role="status"witharia-live="polite"to the loading container androle="alert"to the error container. Screen reader users then learn that the directory finished loading or failed.Also applies to: 125-136
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/mentorship/MentorDirectory.tsx` around lines 119 - 124, Update the loading container in MentorDirectory’s isLoading branch to include role="status" and aria-live="polite", and add role="alert" to the corresponding error container covering the error state. Leave the existing loading and error content unchanged.supabase/migrations/20260825000002_alumni_mentorship.sql (3)
23-24: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win
updated_atis never maintained on row updates.Both tables default
updated_attoNOW()on insert. The capacity trigger updatesmentor_profiles.updated_atexplicitly, but a direct profile update from the alumni UI leaves the value stale.mentorship_requests.updated_atis never refreshed on a status change. Add aBEFORE UPDATEtimestamp trigger for both tables.Also applies to: 38-39
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@supabase/migrations/20260825000002_alumni_mentorship.sql` around lines 23 - 24, Add BEFORE UPDATE timestamp triggers for both mentor_profiles and mentorship_requests so updated_at is refreshed on every row update, including direct profile edits and mentorship request status changes; retain the existing capacity trigger behavior and use a shared trigger function if available.
93-96: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winPrefer
TO authenticatedoverauth.role().The existing migrations grant access with a role clause, for example
supabase/migrations/20260727080000_impossible_travel.sqlLines 36-38.auth.role()reads the JWT claim and behaves differently from the Postgres role. Align with the repository precedent.♻️ Proposed change
CREATE POLICY "Anyone can view mentor profiles" -ON public.mentor_profiles FOR SELECT -USING (auth.role() = 'authenticated'); +ON public.mentor_profiles FOR SELECT TO authenticated +USING (true);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@supabase/migrations/20260825000002_alumni_mentorship.sql` around lines 93 - 96, Update the "Anyone can view mentor profiles" policy to target the authenticated Postgres role with a TO authenticated clause, and remove the auth.role() condition from its USING expression. Follow the existing migration convention while preserving the policy’s SELECT access for authenticated users.
19-19: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy liftModel mentor affiliations as a relational table.
club_affiliationsis a bareUUID[]with no foreign key topublic.clubs, and no code readspublic.mentor_profiles.club_affiliations. If club filtering is required, add a join table with foreign keys tomentor_profiles.user_idandclubs.id, then query it fromuseMentorshipDirectory. Otherwise, remove this column until the feature is implemented.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@supabase/migrations/20260825000002_alumni_mentorship.sql` at line 19, Remove the unused club_affiliations UUID[] column from the mentor_profiles schema unless the feature is being implemented now; if retained, replace it with a mentor-club join table referenced by mentor_profiles.user_id and clubs.id, and update useMentorshipDirectory to query that relationship.src/components/mentorship/RequestMentorshipModal.tsx (2)
121-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAnnounce the error and narrow the caught type.
Add
role="alert"to the error container on Lines 121-125 so assistive technology reports the submission failure. Replacecatch (err: any)on Line 51 withcatch (err: unknown)and narrow witherr instanceof Errorbefore you readmessage. Theanytype also lets a raw PostgREST error message reach the user interface, which can expose schema details.Also applies to: 51-52
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/mentorship/RequestMentorshipModal.tsx` around lines 121 - 125, Update the error container in RequestMentorshipModal to include role="alert" so submission failures are announced to assistive technology. Change the submission handler’s catch clause from err: any to err: unknown, narrow with err instanceof Error before reading message, and use a safe generic fallback for non-Error values instead of exposing raw PostgREST details.
49-55: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winClear the auto-close timer and skip the redundant state update.
Two problems exist on the success path:
- The
setTimeouthandle is never stored. If the parent unmounts the modal before the 2000 ms elapse,onClosestill runs against a stale closure.setIsSubmitting(false)in thefinallyblock runs aftersetSuccess(true). The success branch on Line 58 returns early, so the value is never read.Track the timer in a ref and clear it in a cleanup effect.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/components/mentorship/RequestMentorshipModal.tsx` around lines 49 - 55, Update the success handling in RequestMentorshipModal to store the 2000 ms setTimeout handle in a ref, and add a cleanup effect that clears the timer when the modal unmounts. Remove the redundant setIsSubmitting(false) update from the finally block on the successful early-return path while preserving submission-state cleanup for failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/mentorship/MentorDirectory.tsx`:
- Around line 42-52: Associate all three filter labels in MentorDirectory with
their controls by adding matching htmlFor values and unique id attributes: use
the proposed mentor-industry and mentor-company identifiers for the selects and
a corresponding stable identifier for the search input. Preserve the existing
filter behavior and styling.
- Around line 16-18: Replace the selectedMentorId state and mentors.find lookup
in MentorDirectory with selectedMentor state typed as MentorProfile | null,
importing MentorProfile from the hook. Update the card onRequest callback to
store the mentor object and the modal onClose handler to clear it, so the modal
remains mounted when refetching or filtering removes the mentor from mentors.
In `@src/components/mentorship/MentorProfileCard.tsx`:
- Around line 24-29: Update the avatar img in MentorProfileCard to always
provide a defined alt value by reusing the same fallback as the heading at line
37 when mentor.profiles.full_name is absent.
In `@src/components/mentorship/RequestMentorshipModal.tsx`:
- Around line 25-28: In RequestMentorshipModal, compute the trimmed message
length once and reuse it across validation, the character counter/hint, and the
submit-button disabled condition. Replace direct message.length checks with this
shared trimmed length so all UI states consistently enforce the 50-character
minimum.
- Around line 58-74: Update the success message in RequestMentorshipModal’s
success rendering to use the same “Alumni” fallback as MentorProfileCard when
mentor.profiles?.full_name is unavailable, including the corresponding
mentor-name rendering around the other referenced occurrence. Preserve the
existing name when present.
- Around line 34-47: Update the request submission flow in
RequestMentorshipModal to obtain the authenticated user via the existing
Supabase auth client, require a valid user id, and include it as mentee_id in
the mentorship_requests insert alongside mentor_id, message, and status.
- Around line 76-79: Update RequestMentorshipModal to use the existing Dialog or
Modal primitive for both the request form and “Request Sent!” states instead of
fixed overlay containers. Add accessible titles for “Request Mentorship” and
“Request Sent!”, preserve focus trapping and Escape-to-close behavior, and
ensure closing restores focus to the MentorProfileCard request trigger.
In `@src/hooks/useMentorshipDirectory.ts`:
- Around line 95-101: Move industry and company option loading out of the
filtered fetch flow: add a separate effect that retrieves distinct values once
without depending on filters, and populate industries and companies from that
unfiltered source. Remove the data-based option-building block and the
industries.length dependency from fetchMentors so option state does not freeze
on empty results or trigger redundant filtered queries.
- Around line 4-6: Convert the SQL-style header comments to valid TypeScript
comments in src/hooks/useMentorshipDirectory.ts lines 4-6,
src/components/mentorship/MentorDirectory.tsx lines 4-6,
src/components/mentorship/MentorProfileCard.tsx lines 3-6, and
src/components/mentorship/RequestMentorshipModal.tsx lines 3-5; remove the stray
indentation before the separator in useMentorshipDirectory.ts. Add a lint step
to the pipeline that prevents non-parsing modules from merging.
- Around line 69-72: Update src/hooks/useMentorshipDirectory.ts lines 69-72 to
select first_name, last_name, and avatar_url, and update its profiles type at
lines 21-24 accordingly; export a helper that composes the display name. Ensure
the mentor_profiles-to-profiles foreign-key relationship required by the embed
is declared. In src/components/mentorship/MentorProfileCard.tsx lines 27-37 and
src/components/mentorship/RequestMentorshipModal.tsx lines 69-92, replace each
full_name access with the shared display-name helper.
- Line 9: Update the Supabase import in useMentorshipDirectory.ts to reference
the existing ../lib/supabase/client module instead of the nonexistent
../../lib/supabaseClient path.
- Around line 77-79: Update the search-filter construction in
useMentorshipDirectory to escape filters.search before interpolating it into
query.or(). Escape backslashes and double quotes, then quote the company and
job_title scalar values and the expertise_tags cs array element so commas,
parentheses, and other PostgREST syntax remain literal.
In `@supabase/migrations/20260825000002_alumni_mentorship.sql`:
- Around line 51-52: Update the SECURITY DEFINER function
update_mentor_capacity() to declare an explicit, trusted search_path in its
function definition, including the schema containing mentor_profiles and any
required built-in schema, so object and function resolution cannot be shadowed
by attacker-controlled schemas.
- Around line 60-76: Update the accepted-to-non-accepted branch of the
mentorship status trigger to stop unconditionally setting is_accepting to TRUE.
Preserve a mentor’s manual opt-out; only reopen automatically when the existing
schema can distinguish that is_accepting was closed by the capacity rule,
otherwise leave the value unchanged. Keep the current_mentees decrement and
updated_at updates intact.
- Around line 12-25: Add a foreign key constraint on mentor_profiles.user_id
referencing public.profiles(id), while preserving the existing auth.users(id)
reference and cascade behavior. Ensure the declared relationship supports the
profiles:user_id embedding used by useMentorshipDirectory.
- Around line 41-42: Replace the unconditional UNIQUE(mentor_id, mentee_id)
constraint in the mentorship requests schema with a partial unique index on
mentor_id and mentee_id that applies only to active request statuses. Preserve
the one-active-request-per-pair rule while allowing new requests after rejected
or completed requests.
---
Nitpick comments:
In `@src/components/mentorship/MentorDirectory.tsx`:
- Around line 119-124: Update the loading container in MentorDirectory’s
isLoading branch to include role="status" and aria-live="polite", and add
role="alert" to the corresponding error container covering the error state.
Leave the existing loading and error content unchanged.
In `@src/components/mentorship/MentorProfileCard.tsx`:
- Around line 71-85: Add progressbar accessibility attributes to the inner
capacity-bar div in MentorProfileCard: set role="progressbar", aria-valuenow to
the current capacity percentage, aria-valuemin to 0, and aria-valuemax to 100,
while preserving the existing styling and width calculation.
- Around line 88-98: Update the mentorship request button in MentorProfileCard
to expose why it is unavailable to assistive technology while preserving the
existing disabled behavior and “Currently Full” label. Add an accessible
explanation using aria-disabled with descriptive text or associate the button
with the capacity message via aria-describedby, ensuring the full-state reason
remains perceivable to keyboard and screen-reader users.
In `@src/components/mentorship/RequestMentorshipModal.tsx`:
- Around line 121-125: Update the error container in RequestMentorshipModal to
include role="alert" so submission failures are announced to assistive
technology. Change the submission handler’s catch clause from err: any to err:
unknown, narrow with err instanceof Error before reading message, and use a safe
generic fallback for non-Error values instead of exposing raw PostgREST details.
- Around line 49-55: Update the success handling in RequestMentorshipModal to
store the 2000 ms setTimeout handle in a ref, and add a cleanup effect that
clears the timer when the modal unmounts. Remove the redundant
setIsSubmitting(false) update from the finally block on the successful
early-return path while preserving submission-state cleanup for failures.
In `@src/hooks/useMentorshipDirectory.ts`:
- Around line 103-105: Update the catch clause in useMentorshipDirectory to use
unknown instead of any, then narrow err before accessing message; preserve the
existing fallback error text and console logging behavior.
- Around line 62-63: Update fetchMentors in useMentorshipDirectory to debounce
requests triggered by search changes and cancel or ignore superseded requests,
using an AbortController or cancellation flag so stale responses cannot update
mentor results. Keep loading and result updates tied only to the latest active
request, and clean up the debounce/cancellation state when dependencies change
or the hook unmounts.
In `@supabase/migrations/20260825000002_alumni_mentorship.sql`:
- Around line 23-24: Add BEFORE UPDATE timestamp triggers for both
mentor_profiles and mentorship_requests so updated_at is refreshed on every row
update, including direct profile edits and mentorship request status changes;
retain the existing capacity trigger behavior and use a shared trigger function
if available.
- Around line 93-96: Update the "Anyone can view mentor profiles" policy to
target the authenticated Postgres role with a TO authenticated clause, and
remove the auth.role() condition from its USING expression. Follow the existing
migration convention while preserving the policy’s SELECT access for
authenticated users.
- Line 19: Remove the unused club_affiliations UUID[] column from the
mentor_profiles schema unless the feature is being implemented now; if retained,
replace it with a mentor-club join table referenced by mentor_profiles.user_id
and clubs.id, and update useMentorshipDirectory to query that relationship.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 1efe16cc-5ce6-4c51-b7d7-7278490b6554
📒 Files selected for processing (5)
src/components/mentorship/MentorDirectory.tsxsrc/components/mentorship/MentorProfileCard.tsxsrc/components/mentorship/RequestMentorshipModal.tsxsrc/hooks/useMentorshipDirectory.tssupabase/migrations/20260825000002_alumni_mentorship.sql
Pull Request
Description
Builds an Alumni Mentorship Matching Module allowing alumni to opt-in as mentors with defined capacity limits and expertise tags. Current students can browse the directory, filter by industry/company, and send introductory requests. Includes a Postgres trigger that automatically marks mentors as "Full" and hides them from the available pool once they hit their max_mentees limit.
Type of Change
Related Issue
Closes #2963
Testing
Describe the testing performed.
Verified mentor profiles display correctly with capacity bars. Tested the request flow and confirmed the database trigger accurately updates
is_acceptingwhenmax_menteesis reached.Checklist
Summary by CodeRabbit