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
4 changes: 2 additions & 2 deletions app/api/mentors/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function GET(req: Request) {
const supabase = getSupabaseClient();
let query = supabase
.from("mentor_applications")
.select("id, first_name, last_name, company, occupation, expertise, expertise_areas, mentoring_types, linkedin, availability, created_at")
.select("id, user_id, first_name, last_name, company, occupation, expertise, expertise_areas, mentoring_types, linkedin, availability, created_at")
.eq("status", "approved")
.order("created_at", { ascending: false });

Expand All @@ -43,6 +43,6 @@ export async function GET(req: Request) {
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}

return NextResponse.json({ mentors: data });
}
142 changes: 82 additions & 60 deletions app/protected/mentorship/components/MentorCard.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
"use client";

import { useState } from "react";
import { useRouter } from "next/navigation";
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Briefcase, Linkedin, Clock, GraduationCap } from "lucide-react";
import { Briefcase, Linkedin, MessageCircle, Loader2 } from "lucide-react";
import { RequestDialog } from "./RequestDialog";
import { conversationService } from "@/lib/services/conversationService";
import { toast } from "sonner";

export interface Mentor {
id: string;
user_id?: string | null;
first_name: string;
last_name: string;
company: string;
Expand All @@ -25,32 +30,34 @@ interface MentorCardProps {
mentor: Mentor;
}

const EXPERTISE_LABELS: Record<string, string> = {
"web-development": "Web Dev",
"mobile-development": "Mobile Dev",
"ai-ml": "AI & ML",
"data-science": "Data Science",
"cybersecurity": "Cybersecurity",
"blockchain": "Blockchain",
"ui-ux": "UI/UX",
"devops": "DevOps",
"game-development": "Game Dev",
"cloud-computing": "Cloud",
"system-design": "System Design",
"algorithms": "Algorithms",
};

const IMAGE_MAP: Record<string, string> = {
"Deepak Pandey": "/images/team/deepak.jpeg",
"Parisha Sharma": "/images/team/parisha.jpeg",
"Akshay Kumar": "/images/team/akshay.jpg",
};

export function MentorCard({ mentor }: MentorCardProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const initials = `${mentor.first_name[0]}${mentor.last_name[0]}`;
const fullName = `${mentor.first_name} ${mentor.last_name}`;
const imageSrc = IMAGE_MAP[fullName] || `https://api.dicebear.com/7.x/avataaars/svg?seed=${mentor.id}`;

const handleMessage = async () => {
if (!mentor.user_id) return;

setLoading(true);
try {
const conversation = await conversationService.getOrCreateMentorshipConversation(mentor.user_id);
router.push(`/protected/messages?conversation=${conversation.id}`);
} catch (error) {
console.error("Failed to start conversation:", error);
toast.error("Failed to start conversation. Please try again.");
} finally {
setLoading(false);
}
};

return (
<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">
<CardHeader className="p-6 pb-4 space-y-4">
Expand All @@ -63,77 +70,92 @@ export function MentorCard({ mentor }: MentorCardProps) {
</AvatarFallback>
</Avatar>
<div>
<h3 className="font-bold text-lg leading-none mb-1 group-hover:text-primary transition-colors">
{fullName}
<h3 className="font-bold text-lg text-zinc-900 dark:text-zinc-100 group-hover:text-primary transition-colors">
{mentor.first_name} {mentor.last_name}
</h3>
<div className="flex items-center text-sm text-muted-foreground mb-1">
<Briefcase className="h-3.5 w-3.5 mr-1.5" />
<div className="flex items-center text-sm text-zinc-500 dark:text-zinc-400 mt-1">
<Briefcase className="w-3.5 h-3.5 mr-1.5" />
{mentor.occupation} at {mentor.company}
</div>
{mentor.linkedin && (
<a
href={mentor.linkedin}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center text-xs text-blue-500 hover:underline"
>
<Linkedin className="h-3 w-3 mr-1" />
LinkedIn Profile
</a>
)}
</div>
</div>
{mentor.linkedin && (
<a
href={mentor.linkedin}
target="_blank"
rel="noopener noreferrer"
className="text-zinc-400 hover:text-[#0077b5] transition-colors"
>
<Linkedin className="w-5 h-5" />
</a>
)}
</div>
</CardHeader>

<CardContent className="p-6 pt-0 flex-grow space-y-4">
<div className="space-y-2">
<p className="text-sm text-muted-foreground line-clamp-3">
<CardContent className="px-6 py-2 flex-grow space-y-4">
<div>
<p className="text-sm text-zinc-600 dark:text-zinc-300 line-clamp-2 mb-3">
{mentor.expertise}
</p>
</div>

<div className="space-y-3">
<div className="flex flex-wrap gap-1.5">
{mentor.expertise_areas?.slice(0, 4).map((area) => (
{mentor.expertise_areas.slice(0, 3).map((area) => (
<Badge
key={area}
variant="secondary"
className="text-xs bg-primary/5 text-primary hover:bg-primary/10 border-transparent"
className="text-xs bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700"
>
{EXPERTISE_LABELS[area] || area}
{area.replace("-", " ")}
</Badge>
))}
{mentor.expertise_areas?.length > 4 && (
{mentor.expertise_areas?.length > 3 && (
<Badge variant="outline" className="text-xs">
+{mentor.expertise_areas.length - 4} more
+{mentor.expertise_areas.length - 3} more
</Badge>
)}
</div>
</div>

<div className="flex items-center gap-4 text-xs text-muted-foreground pt-2 border-t border-border/50">
<div className="flex items-center">
<Clock className="h-3.5 w-3.5 mr-1.5" />
{mentor.availability}
</div>
<div className="flex items-center">
<GraduationCap className="h-3.5 w-3.5 mr-1.5" />
{mentor.mentoring_types?.length || 0} Types
<div className="pt-2 border-t border-zinc-100 dark:border-zinc-800">
<div className="flex flex-wrap gap-2 text-xs text-zinc-500">
{mentor.mentoring_types.map((type) => (
<span key={type} className="flex items-center">
• {type.replace("-", " ")}
</span>
))}
</div>
</div>
</CardContent>

<CardFooter className="p-6 pt-0 mt-auto">
<RequestDialog
mentorName={fullName}
mentorId={mentor.id}
trigger={
<Button className="w-full bg-primary/10 text-primary hover:bg-primary/20 shadow-none border-0">
Request Mentorship
</Button>
}
/>
<CardFooter className="p-6 pt-2">
{mentor.user_id ? (
<Button
className="w-full bg-primary/10 hover:bg-primary/20 text-primary border-0 shadow-none"
onClick={handleMessage}
disabled={loading}
>
{loading ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Connecting...
</>
) : (
<>
<MessageCircle className="w-4 h-4 mr-2" />
Message Mentor
</>
)}
</Button>
) : (
<RequestDialog
mentorName={fullName}
mentorId={mentor.id}
trigger={
<Button className="w-full bg-primary/10 text-primary hover:bg-primary/20 shadow-none border-0">
Request Mentorship
</Button>
}
/>
)}
</CardFooter>
</Card>
);
Expand Down
64 changes: 58 additions & 6 deletions lib/services/conversationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class ConversationService {
// Decrypt message content via API
private async decryptContent(encrypted: string | null): Promise<string | null> {
if (!encrypted) return null

try {
const response = await fetch('/api/messages/decrypt', {
method: 'POST',
Expand Down Expand Up @@ -133,9 +133,9 @@ export class ConversationService {
}

if (!data) {
return {
canMessage: false,
reason: 'This user does not accept messages from you. You need to be mutual connections or they need to enable "Allow messages from anyone".'
return {
canMessage: false,
reason: 'This user does not accept messages from you. You need to be mutual connections or they need to enable "Allow messages from anyone".'
}
}

Expand Down Expand Up @@ -171,7 +171,7 @@ export class ConversationService {
.eq('conversation_id', conv.conversation_id)

const participantIds = participants?.map(p => p.user_id) || []

// Check if it's a 1-on-1 with the target user
if (
participantIds.length === 2 &&
Expand Down Expand Up @@ -210,7 +210,8 @@ export class ConversationService {
.from('conversations')
.insert([{
is_group: data.is_group || false,
group_name: data.group_name || null
group_name: data.group_name || null,
conversation_type: data.conversation_type || 'personal'
}])
.select()
.single()
Expand Down Expand Up @@ -281,6 +282,57 @@ export class ConversationService {

return data || []
}

// Get or create a mentorship conversation with a mentor
async getOrCreateMentorshipConversation(mentorUserId: string): Promise<Conversation> {
const supabase = this.getSupabaseClient()
const { data: { user } } = await supabase.auth.getUser()

if (!user) {
throw new Error('User not authenticated')
}

// Check if mentorship conversation already exists
const { data: existingConversations } = await supabase
.from('conversation_participants')
.select('conversation_id')
.eq('user_id', user.id)

if (existingConversations) {
for (const conv of existingConversations) {
const { data: conversation } = await supabase
.from('conversations')
.select('*')
.eq('id', conv.conversation_id)
.eq('conversation_type', 'mentorship')
.single()

if (conversation) {
const { data: participants } = await supabase
.from('conversation_participants')
.select('user_id')
.eq('conversation_id', conv.conversation_id)

const participantIds = participants?.map(p => p.user_id) || []

// Check if it's a 1-on-1 mentorship with the target mentor
if (
participantIds.length === 2 &&
participantIds.includes(user.id) &&
participantIds.includes(mentorUserId)
) {
return conversation as Conversation
}
}
}
}

// Create new mentorship conversation
return this.createConversation({
participant_ids: [user.id, mentorUserId],
conversation_type: 'mentorship'
})
}
}

export const conversationService = new ConversationService()
8 changes: 5 additions & 3 deletions types/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export interface Conversation {
is_group: boolean
group_name: string | null
group_avatar_url: string | null

conversation_type?: 'personal' | 'mentorship' | 'group'

// Computed fields
unread_count?: number
other_user?: {
Expand All @@ -29,7 +30,7 @@ export interface ConversationParticipant {
joined_at: string
last_read_at: string
is_admin: boolean

// User details
user?: {
id: string
Expand All @@ -51,7 +52,7 @@ export interface Message {
is_deleted: boolean
reply_to_id: string | null
attachments: MessageAttachment[] | null

// Sender details
sender?: {
id: string
Expand Down Expand Up @@ -82,6 +83,7 @@ export interface CreateConversationData {
participant_ids: string[]
is_group?: boolean
group_name?: string
conversation_type?: 'personal' | 'mentorship' | 'group'
initial_message?: string
}

Expand Down
Loading