Prettier format pending files

This commit is contained in:
Builder.io 2025-10-18 19:01:07 +00:00
parent 6a8e2f9180
commit c37b8f48ad
6 changed files with 473 additions and 134 deletions

View file

@ -144,8 +144,14 @@ const App = () => (
<Route path="/blog" element={<Blog />} />
<Route path="/blog/:slug" element={<BlogPost />} />
<Route path="/community" element={<Community />} />
<Route path="/community/mentorship" element={<MentorshipRequest />} />
<Route path="/community/mentorship/apply" element={<MentorApply />} />
<Route
path="/community/mentorship"
element={<MentorshipRequest />}
/>
<Route
path="/community/mentorship/apply"
element={<MentorApply />}
/>
<Route path="/community/:tabId" element={<Community />} />
<Route path="/staff" element={<Staff />} />
<Route path="/support" element={<Support />} />

View file

@ -177,20 +177,34 @@ export const aethexSocialService = {
},
// Mentorship
async listMentors(params?: { expertise?: string[]; q?: string; available?: boolean; limit?: number }) {
async listMentors(params?: {
expertise?: string[];
q?: string;
available?: boolean;
limit?: number;
}) {
const qs = new URLSearchParams();
if (params?.expertise?.length) qs.set("expertise", params.expertise.join(","));
if (params?.expertise?.length)
qs.set("expertise", params.expertise.join(","));
if (params?.q) qs.set("q", params.q);
if (typeof params?.available === "boolean") qs.set("available", String(params.available));
if (typeof params?.available === "boolean")
qs.set("available", String(params.available));
if (params?.limit) qs.set("limit", String(params.limit));
const resp = await fetch(`/api/mentors${qs.toString() ? `?${qs.toString()}` : ""}`);
const resp = await fetch(
`/api/mentors${qs.toString() ? `?${qs.toString()}` : ""}`,
);
if (!resp.ok) return [] as any[];
return (await resp.json()) as any[];
},
async applyToBeMentor(
userId: string,
input: { bio?: string | null; expertise: string[]; hourlyRate?: number | null; available?: boolean },
input: {
bio?: string | null;
expertise: string[];
hourlyRate?: number | null;
available?: boolean;
},
) {
const resp = await fetch("/api/mentors/apply", {
method: "POST",
@ -199,19 +213,29 @@ export const aethexSocialService = {
user_id: userId,
bio: input.bio ?? null,
expertise: input.expertise || [],
hourly_rate: typeof input.hourlyRate === "number" ? input.hourlyRate : null,
available: typeof input.available === "boolean" ? input.available : true,
hourly_rate:
typeof input.hourlyRate === "number" ? input.hourlyRate : null,
available:
typeof input.available === "boolean" ? input.available : true,
}),
});
if (!resp.ok) throw new Error(await resp.text());
return await resp.json();
},
async requestMentorship(menteeId: string, mentorId: string, message?: string) {
async requestMentorship(
menteeId: string,
mentorId: string,
message?: string,
) {
const resp = await fetch("/api/mentorship/request", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mentee_id: menteeId, mentor_id: mentorId, message: message || null }),
body: JSON.stringify({
mentee_id: menteeId,
mentor_id: mentorId,
message: message || null,
}),
});
if (!resp.ok) throw new Error(await resp.text());
return await resp.json();
@ -230,11 +254,14 @@ export const aethexSocialService = {
actorId: string,
status: "accepted" | "rejected" | "cancelled",
) {
const resp = await fetch(`/api/mentorship/requests/${encodeURIComponent(id)}/status`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ actor_id: actorId, status }),
});
const resp = await fetch(
`/api/mentorship/requests/${encodeURIComponent(id)}/status`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ actor_id: actorId, status }),
},
);
if (!resp.ok) throw new Error(await resp.text());
return await resp.json();
},

View file

@ -4,7 +4,13 @@ import { useAuth } from "@/contexts/AuthContext";
import { useNavigate } from "react-router-dom";
import { aethexToast } from "@/lib/aethex-toast";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
@ -29,12 +35,18 @@ export default function Staff() {
useEffect(() => {
if (loading) return;
if (!user) {
aethexToast.info({ title: "Sign in required", description: "Staff area requires authentication" });
aethexToast.info({
title: "Sign in required",
description: "Staff area requires authentication",
});
navigate("/login");
return;
}
if (!hasAccess) {
aethexToast.error({ title: "Access denied", description: "You don't have staff permissions" });
aethexToast.error({
title: "Access denied",
description: "You don't have staff permissions",
});
navigate("/dashboard");
}
}, [user, roles, hasAccess, loading, navigate]);
@ -66,15 +78,24 @@ export default function Staff() {
if (user && hasAccess) refresh();
}, [user, hasAccess]);
const updateReportStatus = async (id: string, status: "resolved" | "ignored" | "open") => {
const updateReportStatus = async (
id: string,
status: "resolved" | "ignored" | "open",
) => {
try {
const resp = await fetch(`/api/moderation/reports/${encodeURIComponent(id)}/status`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status }),
});
const resp = await fetch(
`/api/moderation/reports/${encodeURIComponent(id)}/status`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ status }),
},
);
if (resp.ok) {
aethexToast.success({ title: "Updated", description: `Report marked ${status}` });
aethexToast.success({
title: "Updated",
description: `Report marked ${status}`,
});
refresh();
}
} catch {}
@ -84,9 +105,13 @@ export default function Staff() {
<Layout>
<div className="container mx-auto px-4 py-10">
<div className="mb-6">
<Badge variant="outline" className="mb-2">Internal</Badge>
<Badge variant="outline" className="mb-2">
Internal
</Badge>
<h1 className="text-3xl font-bold">Operations Command</h1>
<p className="text-muted-foreground">Staff dashboards, moderation, and internal tools.</p>
<p className="text-muted-foreground">
Staff dashboards, moderation, and internal tools.
</p>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
@ -102,16 +127,26 @@ export default function Staff() {
<Card>
<CardHeader>
<CardTitle>Community Health</CardTitle>
<CardDescription>Quick pulse across posts and reports</CardDescription>
<CardDescription>
Quick pulse across posts and reports
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">Open reports</div>
<div className="text-xl font-semibold">{openReports.length}</div>
<div className="text-sm text-muted-foreground">
Open reports
</div>
<div className="text-xl font-semibold">
{openReports.length}
</div>
</div>
<div className="flex items-center justify-between mt-3">
<div className="text-sm text-muted-foreground">Mentorship requests</div>
<div className="text-xl font-semibold">{mentorshipAll.length}</div>
<div className="text-sm text-muted-foreground">
Mentorship requests
</div>
<div className="text-xl font-semibold">
{mentorshipAll.length}
</div>
</div>
</CardContent>
</Card>
@ -122,11 +157,15 @@ export default function Staff() {
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">Admin API</div>
<div className="text-sm text-muted-foreground">
Admin API
</div>
<Badge className="bg-emerald-600">OK</Badge>
</div>
<div className="flex items-center justify-between mt-3">
<div className="text-sm text-muted-foreground">Notifications</div>
<div className="text-sm text-muted-foreground">
Notifications
</div>
<Badge className="bg-emerald-600">OK</Badge>
</div>
</CardContent>
@ -137,9 +176,15 @@ export default function Staff() {
<CardDescription>Common operational links</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
<Button asChild variant="outline" className="w-full"><a href="/admin">Admin panel</a></Button>
<Button asChild variant="outline" className="w-full"><a href="/community#mentorship">Mentorship hub</a></Button>
<Button asChild variant="outline" className="w-full"><a href="/feed">Community feed</a></Button>
<Button asChild variant="outline" className="w-full">
<a href="/admin">Admin panel</a>
</Button>
<Button asChild variant="outline" className="w-full">
<a href="/community#mentorship">Mentorship hub</a>
</Button>
<Button asChild variant="outline" className="w-full">
<a href="/feed">Community feed</a>
</Button>
</CardContent>
</Card>
</div>
@ -156,19 +201,37 @@ export default function Staff() {
<p className="text-sm text-muted-foreground">Loading</p>
)}
{!loadingData && openReports.length === 0 && (
<p className="text-sm text-muted-foreground">No items in queue.</p>
<p className="text-sm text-muted-foreground">
No items in queue.
</p>
)}
<div className="space-y-3">
{openReports.map((r) => (
<div key={r.id} className="rounded border border-border/50 p-3">
<div
key={r.id}
className="rounded border border-border/50 p-3"
>
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">{r.reason}</div>
<div className="text-xs text-muted-foreground">{r.details}</div>
<div className="text-xs text-muted-foreground">
{r.details}
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => updateReportStatus(r.id, "ignored")}>Ignore</Button>
<Button size="sm" onClick={() => updateReportStatus(r.id, "resolved")}>Resolve</Button>
<Button
variant="outline"
size="sm"
onClick={() => updateReportStatus(r.id, "ignored")}
>
Ignore
</Button>
<Button
size="sm"
onClick={() => updateReportStatus(r.id, "resolved")}
>
Resolve
</Button>
</div>
</div>
</div>
@ -182,25 +245,42 @@ export default function Staff() {
<Card>
<CardHeader>
<CardTitle>Mentorship Requests</CardTitle>
<CardDescription>Review recent mentor/mentee activity</CardDescription>
<CardDescription>
Review recent mentor/mentee activity
</CardDescription>
</CardHeader>
<CardContent>
{loadingData && (
<p className="text-sm text-muted-foreground">Loading</p>
)}
{!loadingData && mentorshipAll.length === 0 && (
<p className="text-sm text-muted-foreground">No requests to review.</p>
<p className="text-sm text-muted-foreground">
No requests to review.
</p>
)}
<div className="space-y-3">
{mentorshipAll.map((req) => (
<div key={req.id} className="rounded border border-border/50 p-3">
<div
key={req.id}
className="rounded border border-border/50 p-3"
>
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">{req.mentee?.full_name || req.mentee?.username} {req.mentor?.full_name || req.mentor?.username}</div>
<div className="text-xs text-muted-foreground">{req.message || "No message"}</div>
<div className="text-sm font-medium">
{req.mentee?.full_name || req.mentee?.username} {" "}
{req.mentor?.full_name || req.mentor?.username}
</div>
<div className="text-xs text-muted-foreground">
{req.message || "No message"}
</div>
</div>
<div className="flex gap-2">
<Badge variant="outline" className="text-xs capitalize">{req.status}</Badge>
<Badge
variant="outline"
className="text-xs capitalize"
>
{req.status}
</Badge>
</div>
</div>
</div>
@ -208,8 +288,12 @@ export default function Staff() {
</div>
<Separator className="my-4" />
<div className="flex gap-2">
<Button asChild><a href="/community/mentorship">Open requests</a></Button>
<Button asChild variant="outline"><a href="/community/mentorship/apply">Mentor directory</a></Button>
<Button asChild>
<a href="/community/mentorship">Open requests</a>
</Button>
<Button asChild variant="outline">
<a href="/community/mentorship/apply">Mentor directory</a>
</Button>
</div>
</CardContent>
</Card>
@ -219,10 +303,14 @@ export default function Staff() {
<Card>
<CardHeader>
<CardTitle>Users</CardTitle>
<CardDescription>Search, roles, and quick actions</CardDescription>
<CardDescription>
Search, roles, and quick actions
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">User tools coming soon.</p>
<p className="text-sm text-muted-foreground">
User tools coming soon.
</p>
</CardContent>
</Card>
</TabsContent>

View file

@ -5,7 +5,13 @@ import { aethexToast } from "@/lib/aethex-toast";
import { aethexSocialService } from "@/lib/aethex-social-service";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
@ -24,7 +30,10 @@ export default function MentorApply() {
useEffect(() => {
if (!loading && !user) {
aethexToast.info({ title: "Sign in required", description: "Please sign in to apply as a mentor" });
aethexToast.info({
title: "Sign in required",
description: "Please sign in to apply as a mentor",
});
navigate("/login");
}
}, [user, loading, navigate]);
@ -35,14 +44,15 @@ export default function MentorApply() {
.map((s) => s.trim())
.filter(Boolean);
if (!parts.length) return;
const next = Array.from(new Set([...expertise, ...parts]))
.slice(0, 20);
const next = Array.from(new Set([...expertise, ...parts])).slice(0, 20);
setExpertise(next);
setExpertiseInput("");
};
const removeExpertise = (tag: string) => {
setExpertise((prev) => prev.filter((t) => t.toLowerCase() !== tag.toLowerCase()));
setExpertise((prev) =>
prev.filter((t) => t.toLowerCase() !== tag.toLowerCase()),
);
};
const canSubmit = useMemo(() => {
@ -58,13 +68,21 @@ export default function MentorApply() {
await aethexSocialService.applyToBeMentor(user.id, {
bio: bio || null,
expertise,
hourlyRate: Number.isFinite(rate as number) ? (rate as number) : undefined,
hourlyRate: Number.isFinite(rate as number)
? (rate as number)
: undefined,
available,
});
aethexToast.success({ title: "Mentor profile saved", description: "You're ready to receive mentorship requests" });
aethexToast.success({
title: "Mentor profile saved",
description: "You're ready to receive mentorship requests",
});
navigate("/community#mentorship");
} catch (e: any) {
aethexToast.error({ title: "Failed to save", description: String(e?.message || e) });
aethexToast.error({
title: "Failed to save",
description: String(e?.message || e),
});
} finally {
setSubmitting(false);
}
@ -74,33 +92,68 @@ export default function MentorApply() {
<Layout>
<div className="container mx-auto px-4 py-12">
<div className="mb-8">
<Badge variant="outline" className="mb-2">Mentorship</Badge>
<Badge variant="outline" className="mb-2">
Mentorship
</Badge>
<h1 className="text-3xl font-bold">Become a mentor</h1>
<p className="text-muted-foreground mt-1">Share your expertise and guide community members through 1:1 sessions and clinics.</p>
<p className="text-muted-foreground mt-1">
Share your expertise and guide community members through 1:1
sessions and clinics.
</p>
</div>
<Card className="max-w-3xl">
<CardHeader>
<CardTitle>Mentor profile</CardTitle>
<CardDescription>Tell mentees how you can help and set your availability.</CardDescription>
<CardDescription>
Tell mentees how you can help and set your availability.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={onSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="bio">Short bio</Label>
<Textarea id="bio" value={bio} onChange={(e) => setBio(e.target.value)} placeholder="What topics do you mentor on?" rows={5} />
<Textarea
id="bio"
value={bio}
onChange={(e) => setBio(e.target.value)}
placeholder="What topics do you mentor on?"
rows={5}
/>
</div>
<div className="space-y-2">
<Label htmlFor="expertise">Expertise</Label>
<div className="flex gap-2">
<Input id="expertise" value={expertiseInput} onChange={(e) => setExpertiseInput(e.target.value)} placeholder="Add tags, e.g. Unreal, AI, Networking" onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addExpertise(); } }} />
<Button type="button" onClick={addExpertise} variant="secondary">Add</Button>
<Input
id="expertise"
value={expertiseInput}
onChange={(e) => setExpertiseInput(e.target.value)}
placeholder="Add tags, e.g. Unreal, AI, Networking"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
addExpertise();
}
}}
/>
<Button
type="button"
onClick={addExpertise}
variant="secondary"
>
Add
</Button>
</div>
{expertise.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{expertise.map((tag) => (
<Badge key={tag} variant="outline" className="cursor-pointer" onClick={() => removeExpertise(tag)}>
<Badge
key={tag}
variant="outline"
className="cursor-pointer"
onClick={() => removeExpertise(tag)}
>
{tag}
</Badge>
))}
@ -111,14 +164,30 @@ export default function MentorApply() {
<div className="grid sm:grid-cols-2 gap-6">
<div className="space-y-2">
<Label htmlFor="rate">Hourly rate (optional)</Label>
<Input id="rate" type="number" min="0" step="1" placeholder="e.g. 100" value={hourlyRate} onChange={(e) => setHourlyRate(e.target.value)} />
<p className="text-xs text-muted-foreground">Set to 0 or leave blank if you mentor for free.</p>
<Input
id="rate"
type="number"
min="0"
step="1"
placeholder="e.g. 100"
value={hourlyRate}
onChange={(e) => setHourlyRate(e.target.value)}
/>
<p className="text-xs text-muted-foreground">
Set to 0 or leave blank if you mentor for free.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="available">Available</Label>
<div className="flex items-center gap-3">
<Switch id="available" checked={available} onCheckedChange={setAvailable} />
<span className="text-sm text-muted-foreground">Show in mentor directory</span>
<Switch
id="available"
checked={available}
onCheckedChange={setAvailable}
/>
<span className="text-sm text-muted-foreground">
Show in mentor directory
</span>
</div>
</div>
</div>
@ -127,7 +196,13 @@ export default function MentorApply() {
<Button type="submit" disabled={!canSubmit}>
{submitting ? "Saving..." : "Save mentor profile"}
</Button>
<Button type="button" variant="outline" onClick={() => navigate("/community#mentorship")}>Cancel</Button>
<Button
type="button"
variant="outline"
onClick={() => navigate("/community#mentorship")}
>
Cancel
</Button>
</div>
</form>
</CardContent>

View file

@ -5,11 +5,23 @@ import { aethexToast } from "@/lib/aethex-toast";
import { aethexSocialService } from "@/lib/aethex-social-service";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useNavigate } from "react-router-dom";
interface MentorRow {
@ -18,7 +30,13 @@ interface MentorRow {
expertise: string[] | null;
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;
user_profiles?: {
id: string;
full_name: string | null;
username: string | null;
avatar_url: string | null;
bio: string | null;
} | null;
}
export default function MentorshipRequest() {
@ -37,7 +55,10 @@ export default function MentorshipRequest() {
useEffect(() => {
if (!loading && !user) {
aethexToast.info({ title: "Sign in required", description: "Please sign in to request mentorship" });
aethexToast.info({
title: "Sign in required",
description: "Please sign in to request mentorship",
});
navigate("/login");
}
}, [user, loading, navigate]);
@ -45,10 +66,18 @@ export default function MentorshipRequest() {
const loadMentors = async () => {
setLoadingMentors(true);
try {
const rows = await aethexSocialService.listMentors({ q: query || undefined, expertise: expertise.length ? expertise : undefined, available: true, limit: 30 });
const rows = await aethexSocialService.listMentors({
q: query || undefined,
expertise: expertise.length ? expertise : undefined,
available: true,
limit: 30,
});
setMentors(rows as MentorRow[]);
} catch (e: any) {
aethexToast.error({ title: "Failed to load mentors", description: String(e?.message || e) });
aethexToast.error({
title: "Failed to load mentors",
description: String(e?.message || e),
});
} finally {
setLoadingMentors(false);
}
@ -65,14 +94,15 @@ export default function MentorshipRequest() {
.map((s) => s.trim())
.filter(Boolean);
if (!parts.length) return;
const next = Array.from(new Set([...expertise, ...parts]))
.slice(0, 20);
const next = Array.from(new Set([...expertise, ...parts])).slice(0, 20);
setExpertise(next);
setExpertiseInput("");
};
const removeExpertise = (tag: string) => {
setExpertise((prev) => prev.filter((t) => t.toLowerCase() !== tag.toLowerCase()));
setExpertise((prev) =>
prev.filter((t) => t.toLowerCase() !== tag.toLowerCase()),
);
};
const onOpenRequest = (m: MentorRow) => {
@ -85,51 +115,100 @@ export default function MentorshipRequest() {
if (!user?.id || !selectedMentor) return;
try {
setSubmitting(true);
await aethexSocialService.requestMentorship(user.id, selectedMentor.user_id, message || undefined);
aethexToast.success({ title: "Request sent", description: "The mentor has been notified" });
await aethexSocialService.requestMentorship(
user.id,
selectedMentor.user_id,
message || undefined,
);
aethexToast.success({
title: "Request sent",
description: "The mentor has been notified",
});
setDialogOpen(false);
} catch (e: any) {
aethexToast.error({ title: "Failed to send", description: String(e?.message || e) });
aethexToast.error({
title: "Failed to send",
description: String(e?.message || e),
});
} finally {
setSubmitting(false);
}
};
const filtersActive = useMemo(() => query.trim().length > 0 || expertise.length > 0, [query, expertise]);
const filtersActive = useMemo(
() => query.trim().length > 0 || expertise.length > 0,
[query, expertise],
);
return (
<Layout>
<div className="container mx-auto px-4 py-12">
<div className="mb-8">
<Badge variant="outline" className="mb-2">Mentorship</Badge>
<Badge variant="outline" className="mb-2">
Mentorship
</Badge>
<h1 className="text-3xl font-bold">Request mentorship</h1>
<p className="text-muted-foreground mt-1">Find mentors by skill and send a short request. Youll be notified when they respond.</p>
<p className="text-muted-foreground mt-1">
Find mentors by skill and send a short request. Youll be notified
when they respond.
</p>
</div>
<Card className="mb-6">
<CardHeader>
<CardTitle>Filters</CardTitle>
<CardDescription>Refine mentors by topic or keyword.</CardDescription>
<CardDescription>
Refine mentors by topic or keyword.
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="q">Search</Label>
<div className="flex gap-2">
<Input id="q" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Name, username, bio" />
<Button variant="secondary" onClick={loadMentors}>Search</Button>
<Input
id="q"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Name, username, bio"
/>
<Button variant="secondary" onClick={loadMentors}>
Search
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="expertise">Expertise</Label>
<div className="flex gap-2">
<Input id="expertise" value={expertiseInput} onChange={(e) => setExpertiseInput(e.target.value)} placeholder="Add tags, e.g. Unreal, AI, Networking" onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addExpertise(); } }} />
<Button type="button" variant="secondary" onClick={addExpertise}>Add</Button>
<Input
id="expertise"
value={expertiseInput}
onChange={(e) => setExpertiseInput(e.target.value)}
placeholder="Add tags, e.g. Unreal, AI, Networking"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
addExpertise();
}
}}
/>
<Button
type="button"
variant="secondary"
onClick={addExpertise}
>
Add
</Button>
</div>
{expertise.length > 0 && (
<div className="flex flex-wrap gap-2 mt-2">
{expertise.map((tag) => (
<Badge key={tag} variant="outline" className="cursor-pointer" onClick={() => removeExpertise(tag)}>
<Badge
key={tag}
variant="outline"
className="cursor-pointer"
onClick={() => removeExpertise(tag)}
>
{tag}
</Badge>
))}
@ -139,7 +218,17 @@ export default function MentorshipRequest() {
</div>
{filtersActive && (
<div className="flex gap-2 mt-4">
<Button variant="outline" onClick={() => { setQuery(""); setExpertise([]); setExpertiseInput(""); loadMentors(); }}>Reset</Button>
<Button
variant="outline"
onClick={() => {
setQuery("");
setExpertise([]);
setExpertiseInput("");
loadMentors();
}}
>
Reset
</Button>
<Button onClick={loadMentors}>Apply filters</Button>
</div>
)}
@ -148,32 +237,49 @@ export default function MentorshipRequest() {
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{loadingMentors && (
<Card><CardContent className="p-6 text-sm text-muted-foreground">Loading mentors...</CardContent></Card>
)}
{!loadingMentors && mentors.length === 0 && (
<Card><CardContent className="p-6 text-sm text-muted-foreground">No mentors found. Try adjusting filters.</CardContent></Card>
)}
{!loadingMentors && mentors.map((m) => (
<Card key={m.user_id} className="flex flex-col">
<CardHeader>
<CardTitle className="text-xl">
{m.user_profiles?.full_name || m.user_profiles?.username || "Mentor"}
</CardTitle>
<CardDescription>
{(m.expertise || []).slice(0, 5).join(", ")}
</CardDescription>
</CardHeader>
<CardContent className="flex-1 space-y-3">
{m.bio && <p className="text-sm text-muted-foreground line-clamp-3">{m.bio}</p>}
{typeof m.hourly_rate === "number" && (
<p className="text-sm">Rate: ${m.hourly_rate}/hr</p>
)}
<div className="pt-2">
<Button onClick={() => onOpenRequest(m)} className="w-full">Request mentorship</Button>
</div>
<Card>
<CardContent className="p-6 text-sm text-muted-foreground">
Loading mentors...
</CardContent>
</Card>
))}
)}
{!loadingMentors && mentors.length === 0 && (
<Card>
<CardContent className="p-6 text-sm text-muted-foreground">
No mentors found. Try adjusting filters.
</CardContent>
</Card>
)}
{!loadingMentors &&
mentors.map((m) => (
<Card key={m.user_id} className="flex flex-col">
<CardHeader>
<CardTitle className="text-xl">
{m.user_profiles?.full_name ||
m.user_profiles?.username ||
"Mentor"}
</CardTitle>
<CardDescription>
{(m.expertise || []).slice(0, 5).join(", ")}
</CardDescription>
</CardHeader>
<CardContent className="flex-1 space-y-3">
{m.bio && (
<p className="text-sm text-muted-foreground line-clamp-3">
{m.bio}
</p>
)}
{typeof m.hourly_rate === "number" && (
<p className="text-sm">Rate: ${m.hourly_rate}/hr</p>
)}
<div className="pt-2">
<Button onClick={() => onOpenRequest(m)} className="w-full">
Request mentorship
</Button>
</div>
</CardContent>
</Card>
))}
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
@ -183,11 +289,21 @@ export default function MentorshipRequest() {
</DialogHeader>
<div className="space-y-2">
<Label htmlFor="msg">Message</Label>
<Textarea id="msg" value={message} onChange={(e) => setMessage(e.target.value)} rows={5} placeholder="Tell the mentor what you want to achieve" />
<Textarea
id="msg"
value={message}
onChange={(e) => setMessage(e.target.value)}
rows={5}
placeholder="Tell the mentor what you want to achieve"
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setDialogOpen(false)}>Cancel</Button>
<Button onClick={onSubmitRequest} disabled={submitting}>{submitting ? "Sending..." : "Send request"}</Button>
<Button variant="outline" onClick={() => setDialogOpen(false)}>
Cancel
</Button>
<Button onClick={onSubmitRequest} disabled={submitting}>
{submitting ? "Sending..." : "Send request"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View file

@ -1183,9 +1183,12 @@ export function createServer() {
// Mentorship API
app.get("/api/mentors", async (req, res) => {
const limit = Math.max(1, Math.min(50, Number(req.query.limit) || 20));
const available = String(req.query.available || "true").toLowerCase() !== "false";
const available =
String(req.query.available || "true").toLowerCase() !== "false";
const expertise = String(req.query.expertise || "").trim();
const q = String(req.query.q || "").trim().toLowerCase();
const q = String(req.query.q || "")
.trim()
.toLowerCase();
try {
const { data, error } = await adminSupabase
.from("mentors")
@ -1206,15 +1209,21 @@ export function createServer() {
.map((s) => s.trim().toLowerCase())
.filter(Boolean);
if (terms.length) {
rows = rows.filter((r: any) =>
Array.isArray(r.expertise) && r.expertise.some((e: string) => terms.includes(String(e).toLowerCase())),
rows = rows.filter(
(r: any) =>
Array.isArray(r.expertise) &&
r.expertise.some((e: string) =>
terms.includes(String(e).toLowerCase()),
),
);
}
}
if (q) {
rows = rows.filter((r: any) => {
const up = (r as any).user_profiles || {};
const name = String(up.full_name || up.username || "").toLowerCase();
const name = String(
up.full_name || up.username || "",
).toLowerCase();
const bio = String(r.bio || up.bio || "").toLowerCase();
return name.includes(q) || bio.includes(q);
});
@ -1226,7 +1235,8 @@ export function createServer() {
});
app.post("/api/mentors/apply", async (req, res) => {
const { user_id, bio, expertise, hourly_rate, available } = (req.body || {}) as {
const { user_id, bio, expertise, hourly_rate, available } = (req.body ||
{}) as {
user_id?: string;
bio?: string | null;
expertise?: string[];
@ -1271,7 +1281,9 @@ export function createServer() {
message?: string | null;
};
if (!mentee_id || !mentor_id) {
return res.status(400).json({ error: "mentee_id and mentor_id required" });
return res
.status(400)
.json({ error: "mentee_id and mentor_id required" });
}
if (mentee_id === mentor_id) {
return res.status(400).json({ error: "cannot request yourself" });
@ -1290,7 +1302,10 @@ export function createServer() {
.select("full_name, username")
.eq("id", mentee_id)
.maybeSingle();
const menteeName = (mentee as any)?.full_name || (mentee as any)?.username || "Someone";
const menteeName =
(mentee as any)?.full_name ||
(mentee as any)?.username ||
"Someone";
await adminSupabase.from("notifications").insert({
user_id: mentor_id,
type: "info",
@ -1345,7 +1360,10 @@ export function createServer() {
if (upErr) return res.status(500).json({ error: upErr.message });
try {
const target = status === "cancelled" ? (reqRow as any).mentor_id : (reqRow as any).mentee_id;
const target =
status === "cancelled"
? (reqRow as any).mentor_id
: (reqRow as any).mentee_id;
const title =
status === "accepted"
? "Mentorship accepted"
@ -1414,14 +1432,23 @@ export function createServer() {
// Moderation API
app.post("/api/moderation/reports", async (req, res) => {
const { reporter_id, target_type, target_id, reason, details } = (req.body || {}) as any;
const { reporter_id, target_type, target_id, reason, details } =
(req.body || {}) as any;
if (!target_type || !reason) {
return res.status(400).json({ error: "target_type and reason required" });
return res
.status(400)
.json({ error: "target_type and reason required" });
}
try {
const { data, error } = await adminSupabase
.from("moderation_reports")
.insert({ reporter_id: reporter_id || null, target_type, target_id: target_id || null, reason, details: details || null } as any)
.insert({
reporter_id: reporter_id || null,
target_type,
target_id: target_id || null,
reason,
details: details || null,
} as any)
.select()
.single();
if (error) return res.status(500).json({ error: error.message });