-
Notifications
You must be signed in to change notification settings - Fork 136
feat: Builds an Alumni Mentorship Matching Module allowing alumni to opt-in as mentors with defined capacity limits and expertise tags #3148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
krushit1307
merged 1 commit into
krushit1307:main
from
Aakif-Kohari:feat/alumni-mentorship-module
Aug 12, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| // ============================================================================= | ||
| // Component: MentorDirectory | ||
| // Issue: #2963 - Build an 'Alumni Mentorship' Matching Module | ||
| --Description: The main directory page where students can browse, filter, | ||
| --and search for alumni mentors.Includes a sidebar for filters and a grid | ||
| --of MentorProfileCards. | ||
| // ============================================================================= | ||
|
|
||
| import React, { useState } from 'react'; | ||
| import { useMentorshipDirectory } from '../../hooks/useMentorshipDirectory'; | ||
| import { MentorProfileCard } from './MentorProfileCard'; | ||
| import { RequestMentorshipModal } from './RequestMentorshipModal'; | ||
|
|
||
| export const MentorDirectory: React.FC = () => { | ||
| const { mentors, isLoading, error, filters, setFilters, industries, companies } = useMentorshipDirectory(); | ||
| const [selectedMentorId, setSelectedMentorId] = useState<string | null>(null); | ||
|
|
||
| const selectedMentor = mentors.find(m => m.user_id === selectedMentorId); | ||
|
|
||
| return ( | ||
| <div className="max-w-7xl mx-auto px-4 py-8"> | ||
| <div className="mb-8"> | ||
| <h1 className="text-3xl font-black text-gray-900 dark:text-white">Alumni Mentor Network</h1> | ||
| <p className="text-gray-600 dark:text-gray-400 mt-2"> | ||
| Connect with alumni who have volunteered to mentor current students in their industry. | ||
| </p> | ||
| </div> | ||
|
|
||
| <div className="grid grid-cols-1 lg:grid-cols-4 gap-8"> | ||
| {/* Filters Sidebar */} | ||
| <aside className="lg:col-span-1 space-y-6"> | ||
| <div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-5 sticky top-24"> | ||
| <h3 className="font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2"> | ||
| <svg className="w-5 h-5 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" /> | ||
| </svg> | ||
| Filters | ||
| </h3> | ||
|
|
||
| {/* Search */} | ||
| <div className="mb-5"> | ||
| <label className="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2"> | ||
| Search | ||
| </label> | ||
| <div className="relative"> | ||
| <input | ||
| type="text" | ||
| value={filters.search} | ||
| onChange={(e) => setFilters({ search: e.target.value })} | ||
| placeholder="Company, role, or skill..." | ||
| className="w-full pl-9 pr-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500" | ||
| /> | ||
|
Aakif-Kohari marked this conversation as resolved.
|
||
| <svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> | ||
| </svg> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Industry Filter */} | ||
| <div className="mb-5"> | ||
| <label className="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2"> | ||
| Industry | ||
| </label> | ||
| <select | ||
| value={filters.industry} | ||
| onChange={(e) => setFilters({ industry: e.target.value })} | ||
| className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500" | ||
| > | ||
| <option value="">All Industries</option> | ||
| {industries.map(ind => ( | ||
| <option key={ind} value={ind}>{ind}</option> | ||
| ))} | ||
| </select> | ||
| </div> | ||
|
|
||
| {/* Company Filter */} | ||
| <div className="mb-5"> | ||
| <label className="block text-xs font-bold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2"> | ||
| Company | ||
| </label> | ||
| <select | ||
| value={filters.company} | ||
| onChange={(e) => setFilters({ company: e.target.value })} | ||
| className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500" | ||
| > | ||
| <option value="">All Companies</option> | ||
| {companies.map(comp => ( | ||
| <option key={comp} value={comp}>{comp}</option> | ||
| ))} | ||
| </select> | ||
| </div> | ||
|
|
||
| {/* Availability Toggle */} | ||
| <div className="pt-4 border-t border-gray-200 dark:border-gray-700"> | ||
| <label className="flex items-center gap-3 cursor-pointer group"> | ||
| <div className="relative"> | ||
| <input | ||
| type="checkbox" | ||
| checked={filters.showAvailableOnly} | ||
| onChange={(e) => setFilters({ showAvailableOnly: e.target.checked })} | ||
| className="sr-only peer" | ||
| /> | ||
| <div className="w-10 h-6 bg-gray-200 dark:bg-gray-700 rounded-full peer peer-checked:bg-indigo-600 transition-colors"></div> | ||
| <div className="absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-transform peer-checked:translate-x-4"></div> | ||
| </div> | ||
| <span className="text-sm font-medium text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-white"> | ||
| Available Only | ||
| </span> | ||
| </label> | ||
| <p className="text-xs text-gray-500 dark:text-gray-400 mt-1 ml-13"> | ||
| Hide mentors who are currently at capacity. | ||
| </p> | ||
| </div> | ||
| </div> | ||
| </aside> | ||
|
|
||
| {/* Mentors Grid */} | ||
| <main className="lg:col-span-3"> | ||
| {isLoading ? ( | ||
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | ||
| {[1, 2, 3, 4].map(i => ( | ||
| <div key={i} className="h-48 bg-gray-100 dark:bg-gray-800 rounded-xl animate-pulse"></div> | ||
| ))} | ||
| </div> | ||
| ) : error ? ( | ||
| <div className="p-8 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl text-red-600 dark:text-red-400 text-center"> | ||
| {error} | ||
| </div> | ||
| ) : mentors.length === 0 ? ( | ||
| <div className="text-center py-16 bg-gray-50 dark:bg-gray-800/50 rounded-xl border-2 border-dashed border-gray-300 dark:border-gray-700"> | ||
| <svg className="w-16 h-16 mx-auto text-gray-400 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /> | ||
| </svg> | ||
| <h3 className="text-lg font-bold text-gray-900 dark:text-white mb-2">No Mentors Found</h3> | ||
| <p className="text-gray-500 dark:text-gray-400">Try adjusting your filters or check back later.</p> | ||
| </div> | ||
| ) : ( | ||
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | ||
| {mentors.map(mentor => ( | ||
| <MentorProfileCard | ||
| key={mentor.user_id} | ||
| mentor={mentor} | ||
| onRequest={() => setSelectedMentorId(mentor.user_id)} | ||
| /> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </main> | ||
| </div> | ||
|
|
||
| {/* Request Modal */} | ||
| {selectedMentor && ( | ||
| <RequestMentorshipModal | ||
| mentor={selectedMentor} | ||
| onClose={() => setSelectedMentorId(null)} | ||
| /> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| // ============================================================================= | ||
| // Component: MentorProfileCard | ||
| -- Issue: #2963 - Build an 'Alumni Mentorship' Matching Module | ||
| -- Description: Renders an individual alumni mentor's profile in the directory | ||
| -- grid. Shows their capacity status, expertise tags, and a CTA to request | ||
| -- mentorship. | ||
| // ============================================================================= | ||
|
|
||
| import React from 'react'; | ||
| import { MentorProfile } from '../../hooks/useMentorshipDirectory'; | ||
|
|
||
| interface MentorProfileCardProps { | ||
| mentor: MentorProfile; | ||
| onRequest: () => void; | ||
| } | ||
|
|
||
| export const MentorProfileCard: React.FC<MentorProfileCardProps> = ({ mentor, onRequest }) => { | ||
| const isFull = !mentor.is_accepting || mentor.current_mentees >= mentor.max_mentees; | ||
| const capacityPct = (mentor.current_mentees / mentor.max_mentees) * 100; | ||
|
|
||
| return ( | ||
| <div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-5 shadow-sm hover:shadow-md transition-all flex flex-col"> | ||
| <div className="flex items-start gap-4 mb-4"> | ||
| {mentor.profiles?.avatar_url ? ( | ||
| <img | ||
| src={mentor.profiles.avatar_url} | ||
| alt={mentor.profiles.full_name} | ||
| className="w-14 h-14 rounded-full object-cover border-2 border-gray-100 dark:border-gray-700 flex-shrink-0" | ||
| /> | ||
|
Aakif-Kohari marked this conversation as resolved.
|
||
| ) : ( | ||
| <div className="w-14 h-14 rounded-full bg-indigo-100 dark:bg-indigo-900/40 flex items-center justify-center text-indigo-700 dark:text-indigo-300 font-bold text-xl flex-shrink-0"> | ||
| {mentor.profiles?.full_name?.charAt(0) || 'A'} | ||
| </div> | ||
| )} | ||
| <div className="flex-1 min-w-0"> | ||
| <h3 className="font-bold text-gray-900 dark:text-white truncate"> | ||
| {mentor.profiles?.full_name || 'Alumni'} | ||
| </h3> | ||
| <p className="text-sm text-indigo-600 dark:text-indigo-400 font-medium truncate"> | ||
| {mentor.job_title} | ||
| </p> | ||
| <p className="text-xs text-gray-500 dark:text-gray-400 truncate"> | ||
| {mentor.company} • {mentor.industry} | ||
| </p> | ||
| </div> | ||
| </div> | ||
|
|
||
| {mentor.bio && ( | ||
| <p className="text-sm text-gray-600 dark:text-gray-300 mb-4 line-clamp-3 flex-1"> | ||
| {mentor.bio} | ||
| </p> | ||
| )} | ||
|
|
||
| {/* Expertise Tags */} | ||
| <div className="flex flex-wrap gap-1.5 mb-4"> | ||
| {mentor.expertise_tags.slice(0, 4).map(tag => ( | ||
| <span | ||
| key={tag} | ||
| className="px-2 py-0.5 text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-full" | ||
| > | ||
| {tag} | ||
| </span> | ||
| ))} | ||
| {mentor.expertise_tags.length > 4 && ( | ||
| <span className="px-2 py-0.5 text-xs font-medium text-gray-500 dark:text-gray-400"> | ||
| +{mentor.expertise_tags.length - 4} more | ||
| </span> | ||
| )} | ||
| </div> | ||
|
|
||
| {/* Capacity Indicator */} | ||
| <div className="mb-4"> | ||
| <div className="flex justify-between text-xs font-medium text-gray-600 dark:text-gray-400 mb-1"> | ||
| <span>Mentorship Capacity</span> | ||
| <span>{mentor.current_mentees}/{mentor.max_mentees}</span> | ||
| </div> | ||
| <div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5"> | ||
| <div | ||
| className={`h-1.5 rounded-full transition-all ${ | ||
| isFull ? 'bg-red-500' : capacityPct > 60 ? 'bg-yellow-500' : 'bg-green-500' | ||
| }`} | ||
| style={{ width: `${Math.min(100, capacityPct)}%` }} | ||
| ></div> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Action Button */} | ||
| <button | ||
| onClick={onRequest} | ||
| disabled={isFull} | ||
| className={`w-full py-2 rounded-lg font-medium text-sm transition-all ${ | ||
| isFull | ||
| ? 'bg-gray-100 dark:bg-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed' | ||
| : 'bg-indigo-600 text-white hover:bg-indigo-700 active:scale-[0.98] shadow-sm' | ||
| }`} | ||
| > | ||
| {isFull ? 'Currently Full' : 'Request Mentorship'} | ||
| </button> | ||
| </div> | ||
| ); | ||
| }; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.