From af610a02487efbd82976d4998f26870aa4648446 Mon Sep 17 00:00:00 2001 From: "Builder.io" Date: Sat, 18 Oct 2025 23:21:39 +0000 Subject: [PATCH] Add AdminMentorshipManager component for managing mentors and requests cgen-4e12d2d7bdb8472fb86d6f3dd5ab1da8 --- .../admin/AdminMentorshipManager.tsx | 397 ++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 client/components/admin/AdminMentorshipManager.tsx diff --git a/client/components/admin/AdminMentorshipManager.tsx b/client/components/admin/AdminMentorshipManager.tsx new file mode 100644 index 00000000..40a66777 --- /dev/null +++ b/client/components/admin/AdminMentorshipManager.tsx @@ -0,0 +1,397 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { aethexToast } from "@/lib/aethex-toast"; +import { cn } from "@/lib/utils"; +import { + Loader2, + RefreshCw, + Save, + Users, + GraduationCap, + MessageSquareText, + DollarSign, + Tag, +} from "lucide-react"; + +interface MentorRow { + user_id: string; + bio?: string | null; + expertise?: string[]; + available?: boolean; + hourly_rate?: number | null; + user_profiles?: { + id?: string; + full_name?: string | null; + username?: string | null; + avatar_url?: string | null; + bio?: string | null; + } | null; +} + +interface MentorshipRequestRow { + id: string; + mentor_id: string; + mentee_id: string; + message?: string | null; + status: "pending" | "accepted" | "rejected" | "cancelled"; + created_at?: string | null; + mentor?: { id?: string; full_name?: string | null; username?: string | null; avatar_url?: string | null } | null; + mentee?: { id?: string; full_name?: string | null; username?: string | null; avatar_url?: string | null } | null; +} + +const statusOptions = ["all", "pending", "accepted", "rejected", "cancelled"] as const; + +type StatusFilter = (typeof statusOptions)[number]; + +export default function AdminMentorshipManager() { + const [mentors, setMentors] = useState([]); + const [loadingMentors, setLoadingMentors] = useState(false); + const [mentorQ, setMentorQ] = useState(""); + const [expertiseInput, setExpertiseInput] = useState(""); + const [expertiseFilter, setExpertiseFilter] = useState([]); + const [availableOnly, setAvailableOnly] = useState(true); + + const [requests, setRequests] = useState([]); + const [loadingRequests, setLoadingRequests] = useState(false); + const [statusFilter, setStatusFilter] = useState("pending"); + + const draftsRef = useRef>>({}); + + const expertiseQueryParam = useMemo(() => { + return expertiseFilter.length ? expertiseFilter.join(",") : ""; + }, [expertiseFilter]); + + const loadMentors = useCallback(async () => { + setLoadingMentors(true); + try { + const params = new URLSearchParams(); + params.set("limit", "50"); + params.set("available", String(availableOnly)); + if (expertiseQueryParam) params.set("expertise", expertiseQueryParam); + if (mentorQ.trim()) params.set("q", mentorQ.trim()); + const resp = await fetch(`/api/mentors?${params.toString()}`); + if (!resp.ok) throw new Error(await resp.text().catch(() => "Failed")); + const data = await resp.json(); + setMentors(Array.isArray(data) ? data : []); + } catch (e: any) { + aethexToast.error({ title: "Failed to load mentors", description: String(e?.message || e) }); + setMentors([]); + } finally { + setLoadingMentors(false); + } + }, [availableOnly, expertiseQueryParam, mentorQ]); + + const loadRequests = useCallback(async () => { + setLoadingRequests(true); + try { + const params = new URLSearchParams(); + params.set("limit", "100"); + if (statusFilter !== "all") params.set("status", statusFilter); + const resp = await fetch(`/api/mentorship/requests/all?${params.toString()}`); + if (!resp.ok) throw new Error(await resp.text().catch(() => "Failed")); + const data = await resp.json(); + setRequests(Array.isArray(data) ? data : []); + } catch (e: any) { + aethexToast.error({ title: "Failed to load requests", description: String(e?.message || e) }); + setRequests([]); + } finally { + setLoadingRequests(false); + } + }, [statusFilter]); + + useEffect(() => { + loadMentors().catch(() => undefined); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [availableOnly, expertiseQueryParam]); + + useEffect(() => { + loadRequests().catch(() => undefined); + }, [loadRequests]); + + const setDraft = (userId: string, patch: Partial) => { + draftsRef.current[userId] = { ...draftsRef.current[userId], ...patch }; + setMentors((prev) => prev.slice()); + }; + + const getDraftedMentor = (m: MentorRow): MentorRow => { + const draft = draftsRef.current[m.user_id] || {}; + return { + ...m, + bio: draft.bio ?? m.bio, + expertise: draft.expertise ?? m.expertise, + available: draft.available ?? m.available, + hourly_rate: draft.hourly_rate ?? m.hourly_rate, + }; + }; + + const saveMentor = async (m: MentorRow) => { + const merged = getDraftedMentor(m); + try { + const payload = { + user_id: merged.user_id, + bio: merged.bio ?? null, + expertise: Array.isArray(merged.expertise) ? merged.expertise : [], + available: !!merged.available, + hourly_rate: typeof merged.hourly_rate === "number" ? merged.hourly_rate : null, + }; + const resp = await fetch("/api/mentors/apply", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (!resp.ok) throw new Error(await resp.text().catch(() => "Save failed")); + const updated = await resp.json(); + draftsRef.current[m.user_id] = {}; + setMentors((prev) => prev.map((row) => (row.user_id === m.user_id ? { ...row, ...updated } : row))); + aethexToast.success({ title: "Mentor saved", description: merged.user_profiles?.full_name || merged.user_profiles?.username || merged.user_id }); + } catch (e: any) { + aethexToast.error({ title: "Save failed", description: String(e?.message || e) }); + } + }; + + const filteredMentors = useMemo(() => { + const q = mentorQ.trim().toLowerCase(); + if (!q) return mentors; + return mentors.filter((m) => { + const up = m.user_profiles || {}; + const haystack = [up.full_name, up.username, m.bio, (m.expertise || []).join(" ")] + .filter(Boolean) + .join(" ") + .toLowerCase(); + return haystack.includes(q); + }); + }, [mentors, mentorQ]); + + return ( +
+ + +
+ + Mentors directory +
+ Search, filter, and update mentor availability. +
+ +
+
+ setMentorQ(e.target.value)} /> +
+
+ + +
+ +
+
+ setExpertiseInput(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + const value = expertiseInput.trim(); + if (value && !expertiseFilter.includes(value)) { + setExpertiseFilter([...expertiseFilter, value]); + } + setExpertiseInput(""); + } + }} + /> + + {expertiseFilter.length > 0 && ( + + )} +
+
+ {expertiseFilter.map((tag) => ( + + {tag} + + ))} +
+ +
+ {loadingMentors ? ( +
Loading mentors…
+ ) : filteredMentors.length === 0 ? ( +
No mentors found.
+ ) : ( + +
+ {filteredMentors.map((m) => { + const draft = draftsRef.current[m.user_id] || {}; + const merged = getDraftedMentor(m); + const up = merged.user_profiles || {}; + return ( +
+
+
+
+ +
{up.full_name || up.username || m.user_id}
+ {merged.available ? "available" : "unavailable"} +
+
{up.username ? `@${up.username}` : null}
+
+ +
+
+ +
+ + setDraft(m.user_id, { hourly_rate: e.target.value === "" ? null : Number(e.target.value) })} + /> +
+
+
+ +
+ setDraft(m.user_id, { available: v })} /> + {merged.available ? "Accepting requests" : "Not accepting"} +
+
+
+ + setDraft(m.user_id, { expertise: e.target.value.split(",").map((s) => s.trim()).filter(Boolean) })} + /> +
+
+
+
+
+ +