Skip to content

Commit df5d8a7

Browse files
committed
feat: introduce mentorship feature with mentor listing, filtering, and API endpoints
1 parent 31791f8 commit df5d8a7

File tree

6 files changed

+564
-0
lines changed

6 files changed

+564
-0
lines changed

app/api/mentors/route.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { NextResponse } from "next/server";
2+
import { createClient } from "@supabase/supabase-js";
3+
4+
// Force Node.js runtime for API routes
5+
export const runtime = 'nodejs';
6+
7+
// Create Supabase client function to avoid build-time initialization
8+
function getSupabaseClient() {
9+
return createClient(
10+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
11+
process.env.SUPABASE_SERVICE_ROLE_KEY!
12+
);
13+
}
14+
15+
// GET: List all approved mentors
16+
export async function GET(req: Request) {
17+
const { searchParams } = new URL(req.url);
18+
const expertise = searchParams.get('expertise');
19+
const type = searchParams.get('type');
20+
const search = searchParams.get('search');
21+
22+
const supabase = getSupabaseClient();
23+
let query = supabase
24+
.from("mentor_applications")
25+
.select("id, first_name, last_name, company, occupation, expertise, expertise_areas, mentoring_types, linkedin, availability, created_at")
26+
.eq("status", "approved")
27+
.order("created_at", { ascending: false });
28+
29+
if (expertise && expertise !== 'all') {
30+
query = query.contains('expertise_areas', [expertise]);
31+
}
32+
33+
if (type && type !== 'all') {
34+
query = query.contains('mentoring_types', [type]);
35+
}
36+
37+
if (search) {
38+
query = query.or(`first_name.ilike.%${search}%,last_name.ilike.%${search}%,company.ilike.%${search}%,occupation.ilike.%${search}%`);
39+
}
40+
41+
const { data, error } = await query;
42+
43+
if (error) {
44+
return NextResponse.json({ error: error.message }, { status: 500 });
45+
}
46+
47+
return NextResponse.json({ mentors: data });
48+
}

app/api/seed-real-mentors/route.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { NextResponse } from "next/server";
2+
import { createClient } from "@supabase/supabase-js";
3+
4+
export const runtime = 'nodejs';
5+
6+
function getSupabaseClient() {
7+
return createClient(
8+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
9+
process.env.SUPABASE_SERVICE_ROLE_KEY!
10+
);
11+
}
12+
13+
const REAL_MENTORS = [
14+
{
15+
first_name: "Deepak",
16+
last_name: "Pandey",
17+
email: "deepak@codeunia.com",
18+
phone: "",
19+
location: "India",
20+
occupation: "Founder & Tech Lead",
21+
company: "Codeunia",
22+
experience: "Full-stack engineer turned founder. Built Codeunia's platform from the ground up. Deep experience in scaling ed-tech products and engineering teams.",
23+
expertise: "Full Stack Development, System Architecture, Startup Engineering, Product Strategy",
24+
linkedin: "https://www.linkedin.com/in/848deepak/",
25+
expertise_areas: ["system-design", "web-development", "cloud-computing"],
26+
mentoring_types: ["career-advice", "project-guidance", "system-design"],
27+
availability: "flexible",
28+
commitment: "occasional",
29+
motivation: "Helping developers bridge the gap between coding tutorials and building production-ready software.",
30+
previous_mentoring: "Guided 100+ students in their transition to professional software engineering roles.",
31+
teaching_style: "Focus on first principles and architectural thinking.",
32+
status: "approved"
33+
},
34+
{
35+
first_name: "Parisha",
36+
last_name: "Sharma",
37+
email: "parisha@codeunia.com",
38+
phone: "",
39+
location: "India",
40+
occupation: "Co-Founder & Operations Lead",
41+
company: "Codeunia",
42+
experience: "Specialist in tech operations and team dynamics. Expert in helping developers navigate their career paths, negotiate offers, and build leadership skills.",
43+
expertise: "Tech Management, Career Strategy, Soft Skills, Agile Methodologies",
44+
linkedin: "https://www.linkedin.com/in/parishasharma93/",
45+
expertise_areas: ["ui-ux", "project-management"], // Mapped to closest available or generic
46+
mentoring_types: ["career-advice", "interview-prep", "one-on-one"],
47+
availability: "weekends",
48+
commitment: "regular",
49+
motivation: "Ensuring developers have the soft skills and strategic mindset needed to succeed in the industry.",
50+
previous_mentoring: "Career coach for early-stage professionals.",
51+
teaching_style: "Empathetic, structured, and goal-oriented coaching.",
52+
status: "approved"
53+
},
54+
{
55+
first_name: "Akshay",
56+
last_name: "Kumar",
57+
email: "akshay.allen26200@gmail.com",
58+
phone: "",
59+
location: "India",
60+
occupation: "Web Development Lead",
61+
company: "Codeunia",
62+
experience: "Senior Frontend Engineer with a focus on performance and user experience. Architected the core learning platform using Next.js and React Server Components.",
63+
expertise: "Advanced React, Next.js, Frontend Architecture, Web Performance",
64+
linkedin: "https://www.linkedin.com/in/akshaykumar0611/",
65+
expertise_areas: ["web-development", "ui-ux", "system-design"],
66+
mentoring_types: ["code-reviews", "project-guidance", "one-on-one"],
67+
availability: "evenings",
68+
commitment: "intensive",
69+
motivation: "Passionate about writing clean, maintainable code and teaching modern web standards.",
70+
previous_mentoring: "Lead code reviewer and technical mentor for Codeunia interns.",
71+
teaching_style: "Hands-on pair programming and detailed code reviews.",
72+
status: "approved"
73+
}
74+
];
75+
76+
export async function GET() {
77+
const supabase = getSupabaseClient();
78+
79+
// 1. Delete existing mentors to reset data
80+
const { error: deleteError } = await supabase
81+
.from("mentor_applications")
82+
.delete()
83+
.neq("id", "00000000-0000-0000-0000-000000000000");
84+
85+
if (deleteError) {
86+
return NextResponse.json({ error: deleteError.message }, { status: 500 });
87+
}
88+
89+
// 2. Insert refined mentors
90+
const { data, error } = await supabase
91+
.from("mentor_applications")
92+
.insert(REAL_MENTORS)
93+
.select();
94+
95+
if (error) {
96+
return NextResponse.json({ error: error.message }, { status: 500 });
97+
}
98+
99+
return NextResponse.json({ success: true, count: data.length, data });
100+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"use client";
2+
3+
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
4+
import { Badge } from "@/components/ui/badge";
5+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
6+
import { Button } from "@/components/ui/button";
7+
import { Briefcase, Linkedin, Clock, GraduationCap } from "lucide-react";
8+
import { RequestDialog } from "./RequestDialog";
9+
10+
export interface Mentor {
11+
id: string;
12+
first_name: string;
13+
last_name: string;
14+
company: string;
15+
occupation: string;
16+
expertise: string;
17+
expertise_areas: string[];
18+
mentoring_types: string[];
19+
linkedin: string;
20+
availability: string;
21+
created_at: string;
22+
}
23+
24+
interface MentorCardProps {
25+
mentor: Mentor;
26+
}
27+
28+
const EXPERTISE_LABELS: Record<string, string> = {
29+
"web-development": "Web Dev",
30+
"mobile-development": "Mobile Dev",
31+
"ai-ml": "AI & ML",
32+
"data-science": "Data Science",
33+
"cybersecurity": "Cybersecurity",
34+
"blockchain": "Blockchain",
35+
"ui-ux": "UI/UX",
36+
"devops": "DevOps",
37+
"game-development": "Game Dev",
38+
"cloud-computing": "Cloud",
39+
"system-design": "System Design",
40+
"algorithms": "Algorithms",
41+
};
42+
43+
const IMAGE_MAP: Record<string, string> = {
44+
"Deepak Pandey": "/images/team/deepak.jpeg",
45+
"Parisha Sharma": "/images/team/parisha.jpeg",
46+
"Akshay Kumar": "/images/team/akshay.jpg",
47+
};
48+
49+
export function MentorCard({ mentor }: MentorCardProps) {
50+
const initials = `${mentor.first_name[0]}${mentor.last_name[0]}`;
51+
const fullName = `${mentor.first_name} ${mentor.last_name}`;
52+
const imageSrc = IMAGE_MAP[fullName] || `https://api.dicebear.com/7.x/avataaars/svg?seed=${mentor.id}`;
53+
54+
return (
55+
<Card className="flex flex-col h-full overflow-hidden border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900/50 hover:shadow-lg transition-all duration-300 group">
56+
<CardHeader className="p-6 pb-4 space-y-4">
57+
<div className="flex justify-between items-start">
58+
<div className="flex gap-4">
59+
<Avatar className="h-16 w-16 border-2 border-primary/10">
60+
<AvatarImage src={imageSrc} className="object-cover" />
61+
<AvatarFallback className="bg-primary/5 text-primary text-lg font-semibold">
62+
{initials}
63+
</AvatarFallback>
64+
</Avatar>
65+
<div>
66+
<h3 className="font-bold text-lg leading-none mb-1 group-hover:text-primary transition-colors">
67+
{fullName}
68+
</h3>
69+
<div className="flex items-center text-sm text-muted-foreground mb-1">
70+
<Briefcase className="h-3.5 w-3.5 mr-1.5" />
71+
{mentor.occupation} at {mentor.company}
72+
</div>
73+
{mentor.linkedin && (
74+
<a
75+
href={mentor.linkedin}
76+
target="_blank"
77+
rel="noopener noreferrer"
78+
className="inline-flex items-center text-xs text-blue-500 hover:underline"
79+
>
80+
<Linkedin className="h-3 w-3 mr-1" />
81+
LinkedIn Profile
82+
</a>
83+
)}
84+
</div>
85+
</div>
86+
</div>
87+
</CardHeader>
88+
89+
<CardContent className="p-6 pt-0 flex-grow space-y-4">
90+
<div className="space-y-2">
91+
<p className="text-sm text-muted-foreground line-clamp-3">
92+
{mentor.expertise}
93+
</p>
94+
</div>
95+
96+
<div className="space-y-3">
97+
<div className="flex flex-wrap gap-1.5">
98+
{mentor.expertise_areas?.slice(0, 4).map((area) => (
99+
<Badge
100+
key={area}
101+
variant="secondary"
102+
className="text-xs bg-primary/5 text-primary hover:bg-primary/10 border-transparent"
103+
>
104+
{EXPERTISE_LABELS[area] || area}
105+
</Badge>
106+
))}
107+
{mentor.expertise_areas?.length > 4 && (
108+
<Badge variant="outline" className="text-xs">
109+
+{mentor.expertise_areas.length - 4} more
110+
</Badge>
111+
)}
112+
</div>
113+
</div>
114+
115+
<div className="flex items-center gap-4 text-xs text-muted-foreground pt-2 border-t border-border/50">
116+
<div className="flex items-center">
117+
<Clock className="h-3.5 w-3.5 mr-1.5" />
118+
{mentor.availability}
119+
</div>
120+
<div className="flex items-center">
121+
<GraduationCap className="h-3.5 w-3.5 mr-1.5" />
122+
{mentor.mentoring_types?.length || 0} Types
123+
</div>
124+
</div>
125+
</CardContent>
126+
127+
<CardFooter className="p-6 pt-0 mt-auto">
128+
<RequestDialog
129+
mentorName={fullName}
130+
mentorId={mentor.id}
131+
trigger={
132+
<Button className="w-full bg-primary/10 text-primary hover:bg-primary/20 shadow-none border-0">
133+
Request Mentorship
134+
</Button>
135+
}
136+
/>
137+
</CardFooter>
138+
</Card>
139+
);
140+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"use client";
2+
3+
import { Input } from "@/components/ui/input";
4+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
5+
import { Search, Filter } from "lucide-react";
6+
7+
interface MentorshipFiltersProps {
8+
search: string;
9+
setSearch: (value: string) => void;
10+
expertise: string;
11+
setExpertise: (value: string) => void;
12+
type: string;
13+
setType: (value: string) => void;
14+
}
15+
16+
const EXPERTISE_OPTIONS = [
17+
{ value: "all", label: "All Expertise" },
18+
{ value: "web-development", label: "Web Development" },
19+
{ value: "mobile-development", label: "Mobile Development" },
20+
{ value: "ai-ml", label: "AI & Machine Learning" },
21+
{ value: "data-science", label: "Data Science" },
22+
{ value: "cybersecurity", label: "Cybersecurity" },
23+
{ value: "blockchain", label: "Blockchain" },
24+
{ value: "ui-ux", label: "UI/UX Design" },
25+
{ value: "devops", label: "DevOps" },
26+
{ value: "game-development", label: "Game Development" },
27+
{ value: "cloud-computing", label: "Cloud Computing" },
28+
{ value: "system-design", label: "System Design" },
29+
{ value: "algorithms", label: "Algorithms" },
30+
];
31+
32+
const TYPE_OPTIONS = [
33+
{ value: "all", label: "All Mentoring Types" },
34+
{ value: "one-on-one", label: "One-on-One" },
35+
{ value: "group-sessions", label: "Group Sessions" },
36+
{ value: "code-reviews", label: "Code Reviews" },
37+
{ value: "project-guidance", label: "Project Guidance" },
38+
{ value: "career-advice", label: "Career Advice" },
39+
{ value: "interview-prep", label: "Interview Prep" },
40+
];
41+
42+
export function MentorshipFilters({
43+
search,
44+
setSearch,
45+
expertise,
46+
setExpertise,
47+
type,
48+
setType,
49+
}: MentorshipFiltersProps) {
50+
return (
51+
<div className="flex flex-col md:flex-row gap-4 items-center bg-card p-4 rounded-lg border shadow-sm">
52+
<div className="relative w-full md:w-1/3">
53+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
54+
<Input
55+
placeholder="Search mentors..."
56+
value={search}
57+
onChange={(e) => setSearch(e.target.value)}
58+
className="pl-10"
59+
/>
60+
</div>
61+
62+
<div className="flex gap-4 w-full md:w-2/3">
63+
<div className="w-1/2">
64+
<Select value={expertise} onValueChange={setExpertise}>
65+
<SelectTrigger>
66+
<div className="flex items-center gap-2">
67+
<Filter className="h-4 w-4 text-muted-foreground" />
68+
<SelectValue placeholder="Expertise" />
69+
</div>
70+
</SelectTrigger>
71+
<SelectContent>
72+
{EXPERTISE_OPTIONS.map((option) => (
73+
<SelectItem key={option.value} value={option.value}>
74+
{option.label}
75+
</SelectItem>
76+
))}
77+
</SelectContent>
78+
</Select>
79+
</div>
80+
81+
<div className="w-1/2">
82+
<Select value={type} onValueChange={setType}>
83+
<SelectTrigger>
84+
<SelectValue placeholder="Mentoring Type" />
85+
</SelectTrigger>
86+
<SelectContent>
87+
{TYPE_OPTIONS.map((option) => (
88+
<SelectItem key={option.value} value={option.value}>
89+
{option.label}
90+
</SelectItem>
91+
))}
92+
</SelectContent>
93+
</Select>
94+
</div>
95+
</div>
96+
</div>
97+
);
98+
}

0 commit comments

Comments
 (0)