Prettier format pending files

This commit is contained in:
Builder.io 2025-10-18 23:24:22 +00:00
parent f26ebaabf9
commit b020510eb0
7 changed files with 451 additions and 105 deletions

View file

@ -119,16 +119,22 @@ const App = () => (
<Route path="/reset-password" element={<ResetPassword />} />
{/* Service routes */}
<Route path="/game-development" element={
<RequireAccess allowedRealms={["game_developer", "staff"]}>
<GameDevelopment />
</RequireAccess>
} />
<Route path="/consulting" element={
<RequireAccess allowedRealms={["client", "staff"]}>
<DevelopmentConsulting />
</RequireAccess>
} />
<Route
path="/game-development"
element={
<RequireAccess allowedRealms={["game_developer", "staff"]}>
<GameDevelopment />
</RequireAccess>
}
/>
<Route
path="/consulting"
element={
<RequireAccess allowedRealms={["client", "staff"]}>
<DevelopmentConsulting />
</RequireAccess>
}
/>
<Route path="/mentorship" element={<MentorshipPrograms />} />
<Route path="/engage" element={<Engage />} />
<Route

View file

@ -2,19 +2,35 @@ import { Navigate, useLocation } from "react-router-dom";
import { useAuth } from "@/contexts/AuthContext";
interface RequireAccessProps {
allowedRealms?: Array<"game_developer" | "client" | "community_member" | "customer" | "staff">;
allowedRealms?: Array<
"game_developer" | "client" | "community_member" | "customer" | "staff"
>;
allowedRoles?: string[];
children: React.ReactElement;
}
export default function RequireAccess({ allowedRealms, allowedRoles, children }: RequireAccessProps) {
export default function RequireAccess({
allowedRealms,
allowedRoles,
children,
}: RequireAccessProps) {
const { user, profile, roles } = useAuth();
const location = useLocation();
const realmOk = !allowedRealms || allowedRealms.includes((profile as any)?.user_type);
const rolesOk = !allowedRoles || (Array.isArray(roles) && roles.some(r => allowedRoles.includes(r.toLowerCase())));
const realmOk =
!allowedRealms || allowedRealms.includes((profile as any)?.user_type);
const rolesOk =
!allowedRoles ||
(Array.isArray(roles) &&
roles.some((r) => allowedRoles.includes(r.toLowerCase())));
if (!user) return <Navigate to={`/onboarding?next=${encodeURIComponent(location.pathname + location.search)}`} replace />;
if (!user)
return (
<Navigate
to={`/onboarding?next=${encodeURIComponent(location.pathname + location.search)}`}
replace
/>
);
if (!realmOk || !rolesOk) return <Navigate to="/realms" replace />;
return children;

View file

@ -12,7 +12,13 @@ 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 {
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";
@ -49,11 +55,27 @@ interface MentorshipRequestRow {
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;
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;
const statusOptions = [
"all",
"pending",
"accepted",
"rejected",
"cancelled",
] as const;
type StatusFilter = (typeof statusOptions)[number];
@ -88,7 +110,10 @@ export default function AdminMentorshipManager() {
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) });
aethexToast.error({
title: "Failed to load mentors",
description: String(e?.message || e),
});
setMentors([]);
} finally {
setLoadingMentors(false);
@ -101,12 +126,17 @@ export default function AdminMentorshipManager() {
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()}`);
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) });
aethexToast.error({
title: "Failed to load requests",
description: String(e?.message || e),
});
setRequests([]);
} finally {
setLoadingRequests(false);
@ -146,20 +176,35 @@ export default function AdminMentorshipManager() {
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,
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"));
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 });
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) });
aethexToast.error({
title: "Save failed",
description: String(e?.message || e),
});
}
};
@ -168,7 +213,12 @@ export default function AdminMentorshipManager() {
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(" ")]
const haystack = [
up.full_name,
up.username,
m.bio,
(m.expertise || []).join(" "),
]
.filter(Boolean)
.join(" ")
.toLowerCase();
@ -184,19 +234,40 @@ export default function AdminMentorshipManager() {
<GraduationCap className="h-5 w-5 text-aethex-300" />
<CardTitle>Mentors directory</CardTitle>
</div>
<CardDescription>Search, filter, and update mentor availability.</CardDescription>
<CardDescription>
Search, filter, and update mentor availability.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap items-center gap-2">
<div className="flex-1 min-w-[240px]">
<Input placeholder="Search mentors" value={mentorQ} onChange={(e) => setMentorQ(e.target.value)} />
<Input
placeholder="Search mentors"
value={mentorQ}
onChange={(e) => setMentorQ(e.target.value)}
/>
</div>
<div className="flex items-center gap-2">
<Switch id="availableOnly" checked={availableOnly} onCheckedChange={setAvailableOnly} />
<Label htmlFor="availableOnly" className="text-sm">Available only</Label>
<Switch
id="availableOnly"
checked={availableOnly}
onCheckedChange={setAvailableOnly}
/>
<Label htmlFor="availableOnly" className="text-sm">
Available only
</Label>
</div>
<Button variant="outline" size="sm" onClick={loadMentors} disabled={loadingMentors}>
{loadingMentors ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCw className="mr-2 h-4 w-4" />}
<Button
variant="outline"
size="sm"
onClick={loadMentors}
disabled={loadingMentors}
>
{loadingMentors ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<RefreshCw className="mr-2 h-4 w-4" />
)}
Refresh
</Button>
</div>
@ -230,7 +301,13 @@ export default function AdminMentorshipManager() {
<Tag className="mr-2 h-4 w-4" /> Add tag
</Button>
{expertiseFilter.length > 0 && (
<Button variant="ghost" size="sm" onClick={() => setExpertiseFilter([])}>Clear tags</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setExpertiseFilter([])}
>
Clear tags
</Button>
)}
</div>
<div className="flex flex-wrap gap-2">
@ -243,9 +320,13 @@ export default function AdminMentorshipManager() {
<div className="rounded border border-border/40">
{loadingMentors ? (
<div className="p-4 text-sm text-muted-foreground">Loading mentors</div>
<div className="p-4 text-sm text-muted-foreground">
Loading mentors
</div>
) : filteredMentors.length === 0 ? (
<div className="p-4 text-sm text-muted-foreground">No mentors found.</div>
<div className="p-4 text-sm text-muted-foreground">
No mentors found.
</div>
) : (
<ScrollArea className="max-h-[560px]">
<div className="divide-y divide-border/40">
@ -259,15 +340,34 @@ export default function AdminMentorshipManager() {
<div className="min-w-[240px]">
<div className="flex items-center gap-2">
<Users className="h-4 w-4 text-aethex-300" />
<div className="font-medium text-foreground">{up.full_name || up.username || m.user_id}</div>
<Badge variant="outline" className={cn("text-xs capitalize", merged.available ? "border-green-500/50 text-green-300" : "border-yellow-500/50 text-yellow-300")}>{merged.available ? "available" : "unavailable"}</Badge>
<div className="font-medium text-foreground">
{up.full_name || up.username || m.user_id}
</div>
<Badge
variant="outline"
className={cn(
"text-xs capitalize",
merged.available
? "border-green-500/50 text-green-300"
: "border-yellow-500/50 text-yellow-300",
)}
>
{merged.available ? "available" : "unavailable"}
</Badge>
</div>
<div className="mt-1 text-xs text-muted-foreground">
{up.username ? `@${up.username}` : null}
</div>
<div className="mt-1 text-xs text-muted-foreground">{up.username ? `@${up.username}` : null}</div>
</div>
<div className="grid gap-2 md:grid-cols-3 flex-1 min-w-[320px]">
<div>
<Label htmlFor={`rate-${m.user_id}`} className="text-xs">Hourly rate (USD)</Label>
<Label
htmlFor={`rate-${m.user_id}`}
className="text-xs"
>
Hourly rate (USD)
</Label>
<div className="flex items-center gap-2">
<DollarSign className="h-4 w-4 text-muted-foreground" />
<Input
@ -275,23 +375,55 @@ export default function AdminMentorshipManager() {
type="number"
min="0"
step="1"
value={typeof merged.hourly_rate === "number" ? merged.hourly_rate : (merged.hourly_rate ?? "")}
onChange={(e) => setDraft(m.user_id, { hourly_rate: e.target.value === "" ? null : Number(e.target.value) })}
value={
typeof merged.hourly_rate === "number"
? merged.hourly_rate
: (merged.hourly_rate ?? "")
}
onChange={(e) =>
setDraft(m.user_id, {
hourly_rate:
e.target.value === ""
? null
: Number(e.target.value),
})
}
/>
</div>
</div>
<div>
<Label className="text-xs">Availability</Label>
<div className="flex items-center gap-2 mt-1">
<Switch checked={!!merged.available} onCheckedChange={(v) => setDraft(m.user_id, { available: v })} />
<span className="text-xs text-muted-foreground">{merged.available ? "Accepting requests" : "Not accepting"}</span>
<Switch
checked={!!merged.available}
onCheckedChange={(v) =>
setDraft(m.user_id, { available: v })
}
/>
<span className="text-xs text-muted-foreground">
{merged.available
? "Accepting requests"
: "Not accepting"}
</span>
</div>
</div>
<div>
<Label className="text-xs">Expertise (comma separated)</Label>
<Label className="text-xs">
Expertise (comma separated)
</Label>
<Input
value={(Array.isArray(merged.expertise) ? merged.expertise : []).join(", ")}
onChange={(e) => setDraft(m.user_id, { expertise: e.target.value.split(",").map((s) => s.trim()).filter(Boolean) })}
value={(Array.isArray(merged.expertise)
? merged.expertise
: []
).join(", ")}
onChange={(e) =>
setDraft(m.user_id, {
expertise: e.target.value
.split(",")
.map((s) => s.trim())
.filter(Boolean),
})
}
/>
</div>
</div>
@ -302,14 +434,22 @@ export default function AdminMentorshipManager() {
<Textarea
rows={2}
value={merged.bio ?? ""}
onChange={(e) => setDraft(m.user_id, { bio: e.target.value })}
onChange={(e) =>
setDraft(m.user_id, { bio: e.target.value })
}
/>
</div>
<div className="flex items-end justify-end gap-2">
<Button size="sm" variant="outline" onClick={() => {
draftsRef.current[m.user_id] = {};
setMentors((prev) => prev.slice());
}}>Reset</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
draftsRef.current[m.user_id] = {};
setMentors((prev) => prev.slice());
}}
>
Reset
</Button>
<Button size="sm" onClick={() => saveMentor(m)}>
<Save className="mr-2 h-4 w-4" /> Save
</Button>
@ -331,24 +471,42 @@ export default function AdminMentorshipManager() {
<MessageSquareText className="h-5 w-5 text-aethex-300" />
<CardTitle>Mentorship requests</CardTitle>
</div>
<CardDescription>Review mentor/mentee activity and statuses.</CardDescription>
<CardDescription>
Review mentor/mentee activity and statuses.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Label className="text-sm">Status</Label>
<Select value={statusFilter} onValueChange={(v) => setStatusFilter(v as StatusFilter)}>
<SelectTrigger className="w-[160px]"><SelectValue placeholder="Status" /></SelectTrigger>
<Select
value={statusFilter}
onValueChange={(v) => setStatusFilter(v as StatusFilter)}
>
<SelectTrigger className="w-[160px]">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
{statusOptions.map((s) => (
<SelectItem key={s} value={s} className="capitalize">{s}</SelectItem>
<SelectItem key={s} value={s} className="capitalize">
{s}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={loadRequests} disabled={loadingRequests}>
{loadingRequests ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCw className="mr-2 h-4 w-4" />}
<Button
variant="outline"
size="sm"
onClick={loadRequests}
disabled={loadingRequests}
>
{loadingRequests ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<RefreshCw className="mr-2 h-4 w-4" />
)}
Refresh
</Button>
<Button size="sm" asChild>
@ -362,9 +520,13 @@ export default function AdminMentorshipManager() {
<div className="rounded border border-border/40">
{loadingRequests ? (
<div className="p-4 text-sm text-muted-foreground">Loading requests</div>
<div className="p-4 text-sm text-muted-foreground">
Loading requests
</div>
) : requests.length === 0 ? (
<div className="p-4 text-sm text-muted-foreground">No requests found.</div>
<div className="p-4 text-sm text-muted-foreground">
No requests found.
</div>
) : (
<ScrollArea className="max-h-[560px]">
<div className="divide-y divide-border/40">
@ -373,15 +535,27 @@ export default function AdminMentorshipManager() {
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-sm font-medium">
{(r.mentee?.full_name || r.mentee?.username || r.mentee_id) + " → " + (r.mentor?.full_name || r.mentor?.username || r.mentor_id)}
{(r.mentee?.full_name ||
r.mentee?.username ||
r.mentee_id) +
" → " +
(r.mentor?.full_name ||
r.mentor?.username ||
r.mentor_id)}
</div>
<div className="text-xs text-muted-foreground mt-0.5">
{r.message || "No message"}
</div>
<div className="text-xs text-muted-foreground mt-0.5">{r.message || "No message"}</div>
{r.created_at ? (
<div className="text-[11px] text-muted-foreground mt-1">{new Date(r.created_at).toLocaleString()}</div>
<div className="text-[11px] text-muted-foreground mt-1">
{new Date(r.created_at).toLocaleString()}
</div>
) : null}
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="capitalize">{r.status}</Badge>
<Badge variant="outline" className="capitalize">
{r.status}
</Badge>
</div>
</div>
</div>

View file

@ -1,10 +1,22 @@
import { useEffect, useMemo, useState } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { AethexUserProfile } from "@/lib/aethex-database-adapter";
import { ArrowDown, ArrowUp, Plus, Save, Trash2, Users } from "lucide-react";
@ -13,7 +25,13 @@ interface SpotlightEntry {
type: "developer" | "group";
label: string;
url?: string;
realms?: ("game_developer" | "client" | "community_member" | "customer" | "staff")[];
realms?: (
| "game_developer"
| "client"
| "community_member"
| "customer"
| "staff"
)[];
}
const STORAGE_KEY = "aethex_spotlights";
@ -33,7 +51,11 @@ function saveSpotlights(entries: SpotlightEntry[]) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
}
export default function AdminSpotlightManager({ profiles }: { profiles: AethexUserProfile[] }) {
export default function AdminSpotlightManager({
profiles,
}: {
profiles: AethexUserProfile[];
}) {
const [entries, setEntries] = useState<SpotlightEntry[]>([]);
const [newGroupName, setNewGroupName] = useState("");
const [newGroupUrl, setNewGroupUrl] = useState("");
@ -43,26 +65,48 @@ export default function AdminSpotlightManager({ profiles }: { profiles: AethexUs
setEntries(loadSpotlights());
}, []);
const devOptions = useMemo(() => profiles.map(p => ({ id: p.id, label: p.full_name || p.username || p.email || "Unknown" })), [profiles]);
const devOptions = useMemo(
() =>
profiles.map((p) => ({
id: p.id,
label: p.full_name || p.username || p.email || "Unknown",
})),
[profiles],
);
const addDeveloper = () => {
if (!selectedProfileId) return;
const profile = profiles.find(p => p.id === selectedProfileId);
const profile = profiles.find((p) => p.id === selectedProfileId);
if (!profile) return;
const label = profile.full_name || profile.username || profile.email || selectedProfileId;
setEntries(prev => [...prev, { id: selectedProfileId, type: "developer", label }]);
const label =
profile.full_name ||
profile.username ||
profile.email ||
selectedProfileId;
setEntries((prev) => [
...prev,
{ id: selectedProfileId, type: "developer", label },
]);
setSelectedProfileId("");
};
const addGroup = () => {
if (!newGroupName.trim()) return;
setEntries(prev => [...prev, { id: crypto.randomUUID(), type: "group", label: newGroupName.trim(), url: newGroupUrl.trim() || undefined }]);
setEntries((prev) => [
...prev,
{
id: crypto.randomUUID(),
type: "group",
label: newGroupName.trim(),
url: newGroupUrl.trim() || undefined,
},
]);
setNewGroupName("");
setNewGroupUrl("");
};
const move = (index: number, dir: -1 | 1) => {
setEntries(prev => {
setEntries((prev) => {
const next = prev.slice();
const j = index + dir;
if (j < 0 || j >= next.length) return prev;
@ -74,7 +118,7 @@ export default function AdminSpotlightManager({ profiles }: { profiles: AethexUs
};
const remove = (index: number) => {
setEntries(prev => prev.filter((_, i) => i !== index));
setEntries((prev) => prev.filter((_, i) => i !== index));
};
const saveAll = () => saveSpotlights(entries);
@ -86,65 +130,126 @@ export default function AdminSpotlightManager({ profiles }: { profiles: AethexUs
<Users className="h-5 w-5 text-aethex-300" />
<CardTitle>Community spotlight</CardTitle>
</div>
<CardDescription>Feature developers and groups on the Community page. Persists locally for now.</CardDescription>
<CardDescription>
Feature developers and groups on the Community page. Persists locally
for now.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<div className="text-sm font-medium">Add developer</div>
<div className="flex gap-2">
<Select value={selectedProfileId} onValueChange={setSelectedProfileId}>
<Select
value={selectedProfileId}
onValueChange={setSelectedProfileId}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Select a profile" />
</SelectTrigger>
<SelectContent>
{devOptions.map(opt => (
<SelectItem key={opt.id} value={opt.id}>{opt.label}</SelectItem>
{devOptions.map((opt) => (
<SelectItem key={opt.id} value={opt.id}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={addDeveloper}><Plus className="h-4 w-4" /></Button>
<Button onClick={addDeveloper}>
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
<div className="space-y-2">
<div className="text-sm font-medium">Add group</div>
<div className="grid gap-2 md:grid-cols-2">
<Input placeholder="Group name" value={newGroupName} onChange={e => setNewGroupName(e.target.value)} />
<Input placeholder="Link (optional)" value={newGroupUrl} onChange={e => setNewGroupUrl(e.target.value)} />
<Input
placeholder="Group name"
value={newGroupName}
onChange={(e) => setNewGroupName(e.target.value)}
/>
<Input
placeholder="Link (optional)"
value={newGroupUrl}
onChange={(e) => setNewGroupUrl(e.target.value)}
/>
</div>
<Button onClick={addGroup} className="mt-1"><Plus className="h-4 w-4 mr-2" />Add group</Button>
<Button onClick={addGroup} className="mt-1">
<Plus className="h-4 w-4 mr-2" />
Add group
</Button>
</div>
</div>
<div className="rounded border border-border/40 bg-background/40">
<div className="flex items-center justify-between p-3">
<div className="text-sm font-medium">Spotlight queue</div>
<Button size="sm" variant="outline" onClick={saveAll}><Save className="h-4 w-4 mr-2" /> Save</Button>
<Button size="sm" variant="outline" onClick={saveAll}>
<Save className="h-4 w-4 mr-2" /> Save
</Button>
</div>
<ScrollArea className="max-h-64">
<ul className="grid gap-2 p-3">
{entries.map((e, i) => (
<li key={`${e.type}-${e.id}-${i}`} className="flex items-center justify-between gap-2 rounded border border-border/30 bg-background/40 p-2">
<li
key={`${e.type}-${e.id}-${i}`}
className="flex items-center justify-between gap-2 rounded border border-border/30 bg-background/40 p-2"
>
<div className="flex items-center gap-2">
<Badge variant="outline" className="capitalize">{e.type}</Badge>
<Badge variant="outline" className="capitalize">
{e.type}
</Badge>
<span className="text-sm text-foreground">{e.label}</span>
{e.url ? <a className="text-xs text-aethex-300 underline" href={e.url} target="_blank" rel="noreferrer">open</a> : null}
{e.url ? (
<a
className="text-xs text-aethex-300 underline"
href={e.url}
target="_blank"
rel="noreferrer"
>
open
</a>
) : null}
</div>
<div className="flex items-center gap-1">
<Button size="icon" variant="outline" onClick={() => move(i, -1)}><ArrowUp className="h-4 w-4" /></Button>
<Button size="icon" variant="outline" onClick={() => move(i, +1)}><ArrowDown className="h-4 w-4" /></Button>
<Button size="icon" variant="outline" onClick={() => remove(i)}><Trash2 className="h-4 w-4" /></Button>
<Button
size="icon"
variant="outline"
onClick={() => move(i, -1)}
>
<ArrowUp className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="outline"
onClick={() => move(i, +1)}
>
<ArrowDown className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="outline"
onClick={() => remove(i)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</li>
))}
{!entries.length && (
<li className="text-sm text-muted-foreground px-3 pb-3">No spotlight entries yet.</li>
<li className="text-sm text-muted-foreground px-3 pb-3">
No spotlight entries yet.
</li>
)}
</ul>
</ScrollArea>
</div>
<p className="text-xs text-muted-foreground">Tip: After saving, visit /community#featured-developers or /community#featured-studios. A backend table can replace local persistence later.</p>
<p className="text-xs text-muted-foreground">
Tip: After saving, visit /community#featured-developers or
/community#featured-studios. A backend table can replace local
persistence later.
</p>
</CardContent>
</Card>
);

View file

@ -1,5 +1,11 @@
import { useEffect, useState } from "react";
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";
interface Studio {
@ -30,13 +36,26 @@ export default function FeaturedStudiosGrid() {
})();
}, []);
if (loading) return <p className="text-sm text-muted-foreground text-center">Loading studios</p>;
if (!studios.length) return <p className="text-sm text-muted-foreground text-center">No featured studios yet.</p>;
if (loading)
return (
<p className="text-sm text-muted-foreground text-center">
Loading studios
</p>
);
if (!studios.length)
return (
<p className="text-sm text-muted-foreground text-center">
No featured studios yet.
</p>
);
return (
<div className="grid gap-6 md:grid-cols-3">
{studios.map((s) => (
<Card key={s.id || s.name} className="border-border/50 bg-background/80 backdrop-blur">
<Card
key={s.id || s.name}
className="border-border/50 bg-background/80 backdrop-blur"
>
<CardHeader>
<CardTitle className="text-lg">{s.name}</CardTitle>
{s.tagline ? <CardDescription>{s.tagline}</CardDescription> : null}

View file

@ -736,12 +736,27 @@ export default function Admin() {
trend={stat.trend}
icon={stat.icon}
tone={stat.tone}
actions={stat.title === "Featured studios" ? (
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={() => navigate("/community#featured-studios")}>Open community</Button>
<Button size="sm" onClick={() => setActiveTab("operations")}>Manage studios</Button>
</div>
) : undefined}
actions={
stat.title === "Featured studios" ? (
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() =>
navigate("/community#featured-studios")
}
>
Open community
</Button>
<Button
size="sm"
onClick={() => setActiveTab("operations")}
>
Manage studios
</Button>
</div>
) : undefined
}
/>
))}
</div>
@ -1180,7 +1195,15 @@ export default function Admin() {
>
Add studio
</Button>
<Button size="sm" variant="outline" onClick={() => navigate("/community#featured-studios")}>Open community</Button>
<Button
size="sm"
variant="outline"
onClick={() =>
navigate("/community#featured-studios")
}
>
Open community
</Button>
</div>
<Button
size="sm"

View file

@ -3226,7 +3226,10 @@ export default function Community() {
{activeTab === "featured" && (
<TabsContent value="featured" className="mt-0">
{/* Featured Developers */}
<section id="featured-developers" className="py-20 bg-background/30">
<section
id="featured-developers"
className="py-20 bg-background/30"
>
<div className="container mx-auto px-4">
<SectionHeader
badge="Community Leaders"