Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions src/components/mentorship/MentorDirectory.tsx
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);
Comment thread
Aakif-Kohari marked this conversation as resolved.

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"
/>
Comment thread
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>
);
};
101 changes: 101 additions & 0 deletions src/components/mentorship/MentorProfileCard.tsx
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"
/>
Comment thread
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>
);
};
Loading
Loading