Skip to content

Commit 871a03e

Browse files
committed
feat: Enable direct messaging with mentors from their cards by fetching user IDs and integrating conversation initiation.
1 parent c26f896 commit 871a03e

File tree

4 files changed

+147
-71
lines changed

4 files changed

+147
-71
lines changed

app/api/mentors/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export async function GET(req: Request) {
2222
const supabase = getSupabaseClient();
2323
let query = supabase
2424
.from("mentor_applications")
25-
.select("id, first_name, last_name, company, occupation, expertise, expertise_areas, mentoring_types, linkedin, availability, created_at")
25+
.select("id, user_id, first_name, last_name, company, occupation, expertise, expertise_areas, mentoring_types, linkedin, availability, created_at")
2626
.eq("status", "approved")
2727
.order("created_at", { ascending: false });
2828

@@ -43,6 +43,6 @@ export async function GET(req: Request) {
4343
if (error) {
4444
return NextResponse.json({ error: error.message }, { status: 500 });
4545
}
46-
46+
4747
return NextResponse.json({ mentors: data });
4848
}

app/protected/mentorship/components/MentorCard.tsx

Lines changed: 82 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
"use client";
22

3+
import { useState } from "react";
4+
import { useRouter } from "next/navigation";
35
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";
46
import { Badge } from "@/components/ui/badge";
57
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
68
import { Button } from "@/components/ui/button";
7-
import { Briefcase, Linkedin, Clock, GraduationCap } from "lucide-react";
9+
import { Briefcase, Linkedin, MessageCircle, Loader2 } from "lucide-react";
810
import { RequestDialog } from "./RequestDialog";
11+
import { conversationService } from "@/lib/services/conversationService";
12+
import { toast } from "sonner";
913

1014
export interface Mentor {
1115
id: string;
16+
user_id?: string | null;
1217
first_name: string;
1318
last_name: string;
1419
company: string;
@@ -25,32 +30,34 @@ interface MentorCardProps {
2530
mentor: Mentor;
2631
}
2732

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-
4333
const IMAGE_MAP: Record<string, string> = {
4434
"Deepak Pandey": "/images/team/deepak.jpeg",
4535
"Parisha Sharma": "/images/team/parisha.jpeg",
4636
"Akshay Kumar": "/images/team/akshay.jpg",
4737
};
4838

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

46+
const handleMessage = async () => {
47+
if (!mentor.user_id) return;
48+
49+
setLoading(true);
50+
try {
51+
const conversation = await conversationService.getOrCreateMentorshipConversation(mentor.user_id);
52+
router.push(`/protected/messages?conversation=${conversation.id}`);
53+
} catch (error) {
54+
console.error("Failed to start conversation:", error);
55+
toast.error("Failed to start conversation. Please try again.");
56+
} finally {
57+
setLoading(false);
58+
}
59+
};
60+
5461
return (
5562
<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">
5663
<CardHeader className="p-6 pb-4 space-y-4">
@@ -63,77 +70,92 @@ export function MentorCard({ mentor }: MentorCardProps) {
6370
</AvatarFallback>
6471
</Avatar>
6572
<div>
66-
<h3 className="font-bold text-lg leading-none mb-1 group-hover:text-primary transition-colors">
67-
{fullName}
73+
<h3 className="font-bold text-lg text-zinc-900 dark:text-zinc-100 group-hover:text-primary transition-colors">
74+
{mentor.first_name} {mentor.last_name}
6875
</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" />
76+
<div className="flex items-center text-sm text-zinc-500 dark:text-zinc-400 mt-1">
77+
<Briefcase className="w-3.5 h-3.5 mr-1.5" />
7178
{mentor.occupation} at {mentor.company}
7279
</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-
)}
8480
</div>
8581
</div>
82+
{mentor.linkedin && (
83+
<a
84+
href={mentor.linkedin}
85+
target="_blank"
86+
rel="noopener noreferrer"
87+
className="text-zinc-400 hover:text-[#0077b5] transition-colors"
88+
>
89+
<Linkedin className="w-5 h-5" />
90+
</a>
91+
)}
8692
</div>
8793
</CardHeader>
8894

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">
95+
<CardContent className="px-6 py-2 flex-grow space-y-4">
96+
<div>
97+
<p className="text-sm text-zinc-600 dark:text-zinc-300 line-clamp-2 mb-3">
9298
{mentor.expertise}
9399
</p>
94-
</div>
95-
96-
<div className="space-y-3">
97100
<div className="flex flex-wrap gap-1.5">
98-
{mentor.expertise_areas?.slice(0, 4).map((area) => (
101+
{mentor.expertise_areas.slice(0, 3).map((area) => (
99102
<Badge
100103
key={area}
101104
variant="secondary"
102-
className="text-xs bg-primary/5 text-primary hover:bg-primary/10 border-transparent"
105+
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"
103106
>
104-
{EXPERTISE_LABELS[area] || area}
107+
{area.replace("-", " ")}
105108
</Badge>
106109
))}
107-
{mentor.expertise_areas?.length > 4 && (
110+
{mentor.expertise_areas?.length > 3 && (
108111
<Badge variant="outline" className="text-xs">
109-
+{mentor.expertise_areas.length - 4} more
112+
+{mentor.expertise_areas.length - 3} more
110113
</Badge>
111114
)}
112115
</div>
113116
</div>
114117

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
118+
<div className="pt-2 border-t border-zinc-100 dark:border-zinc-800">
119+
<div className="flex flex-wrap gap-2 text-xs text-zinc-500">
120+
{mentor.mentoring_types.map((type) => (
121+
<span key={type} className="flex items-center">
122+
{type.replace("-", " ")}
123+
</span>
124+
))}
123125
</div>
124126
</div>
125127
</CardContent>
126128

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-
/>
129+
<CardFooter className="p-6 pt-2">
130+
{mentor.user_id ? (
131+
<Button
132+
className="w-full bg-primary/10 hover:bg-primary/20 text-primary border-0 shadow-none"
133+
onClick={handleMessage}
134+
disabled={loading}
135+
>
136+
{loading ? (
137+
<>
138+
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
139+
Connecting...
140+
</>
141+
) : (
142+
<>
143+
<MessageCircle className="w-4 h-4 mr-2" />
144+
Message Mentor
145+
</>
146+
)}
147+
</Button>
148+
) : (
149+
<RequestDialog
150+
mentorName={fullName}
151+
mentorId={mentor.id}
152+
trigger={
153+
<Button className="w-full bg-primary/10 text-primary hover:bg-primary/20 shadow-none border-0">
154+
Request Mentorship
155+
</Button>
156+
}
157+
/>
158+
)}
137159
</CardFooter>
138160
</Card>
139161
);

lib/services/conversationService.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export class ConversationService {
99
// Decrypt message content via API
1010
private async decryptContent(encrypted: string | null): Promise<string | null> {
1111
if (!encrypted) return null
12-
12+
1313
try {
1414
const response = await fetch('/api/messages/decrypt', {
1515
method: 'POST',
@@ -133,9 +133,9 @@ export class ConversationService {
133133
}
134134

135135
if (!data) {
136-
return {
137-
canMessage: false,
138-
reason: 'This user does not accept messages from you. You need to be mutual connections or they need to enable "Allow messages from anyone".'
136+
return {
137+
canMessage: false,
138+
reason: 'This user does not accept messages from you. You need to be mutual connections or they need to enable "Allow messages from anyone".'
139139
}
140140
}
141141

@@ -171,7 +171,7 @@ export class ConversationService {
171171
.eq('conversation_id', conv.conversation_id)
172172

173173
const participantIds = participants?.map(p => p.user_id) || []
174-
174+
175175
// Check if it's a 1-on-1 with the target user
176176
if (
177177
participantIds.length === 2 &&
@@ -210,7 +210,8 @@ export class ConversationService {
210210
.from('conversations')
211211
.insert([{
212212
is_group: data.is_group || false,
213-
group_name: data.group_name || null
213+
group_name: data.group_name || null,
214+
conversation_type: data.conversation_type || 'personal'
214215
}])
215216
.select()
216217
.single()
@@ -281,6 +282,57 @@ export class ConversationService {
281282

282283
return data || []
283284
}
285+
286+
// Get or create a mentorship conversation with a mentor
287+
async getOrCreateMentorshipConversation(mentorUserId: string): Promise<Conversation> {
288+
const supabase = this.getSupabaseClient()
289+
const { data: { user } } = await supabase.auth.getUser()
290+
291+
if (!user) {
292+
throw new Error('User not authenticated')
293+
}
294+
295+
// Check if mentorship conversation already exists
296+
const { data: existingConversations } = await supabase
297+
.from('conversation_participants')
298+
.select('conversation_id')
299+
.eq('user_id', user.id)
300+
301+
if (existingConversations) {
302+
for (const conv of existingConversations) {
303+
const { data: conversation } = await supabase
304+
.from('conversations')
305+
.select('*')
306+
.eq('id', conv.conversation_id)
307+
.eq('conversation_type', 'mentorship')
308+
.single()
309+
310+
if (conversation) {
311+
const { data: participants } = await supabase
312+
.from('conversation_participants')
313+
.select('user_id')
314+
.eq('conversation_id', conv.conversation_id)
315+
316+
const participantIds = participants?.map(p => p.user_id) || []
317+
318+
// Check if it's a 1-on-1 mentorship with the target mentor
319+
if (
320+
participantIds.length === 2 &&
321+
participantIds.includes(user.id) &&
322+
participantIds.includes(mentorUserId)
323+
) {
324+
return conversation as Conversation
325+
}
326+
}
327+
}
328+
}
329+
330+
// Create new mentorship conversation
331+
return this.createConversation({
332+
participant_ids: [user.id, mentorUserId],
333+
conversation_type: 'mentorship'
334+
})
335+
}
284336
}
285337

286338
export const conversationService = new ConversationService()

types/messaging.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export interface Conversation {
99
is_group: boolean
1010
group_name: string | null
1111
group_avatar_url: string | null
12-
12+
conversation_type?: 'personal' | 'mentorship' | 'group'
13+
1314
// Computed fields
1415
unread_count?: number
1516
other_user?: {
@@ -29,7 +30,7 @@ export interface ConversationParticipant {
2930
joined_at: string
3031
last_read_at: string
3132
is_admin: boolean
32-
33+
3334
// User details
3435
user?: {
3536
id: string
@@ -51,7 +52,7 @@ export interface Message {
5152
is_deleted: boolean
5253
reply_to_id: string | null
5354
attachments: MessageAttachment[] | null
54-
55+
5556
// Sender details
5657
sender?: {
5758
id: string
@@ -82,6 +83,7 @@ export interface CreateConversationData {
8283
participant_ids: string[]
8384
is_group?: boolean
8485
group_name?: string
86+
conversation_type?: 'personal' | 'mentorship' | 'group'
8587
initial_message?: string
8688
}
8789

0 commit comments

Comments
 (0)