aethex-forge/client/components/admin/AdminSpotlightManager.tsx
Builder.io dad2aa6e53 completionId: cgen-843a6775d0874a2dac42f4c899fd4e5e
cgen-843a6775d0874a2dac42f4c899fd4e5e
2025-11-15 02:09:11 +00:00

257 lines
7.8 KiB
TypeScript

import { useEffect, useMemo, useState } from "react";
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 { generateUUID } from "@/lib/utils";
import type { AethexUserProfile } from "@/lib/aethex-database-adapter";
import { ArrowDown, ArrowUp, Plus, Save, Trash2, Users } from "lucide-react";
interface SpotlightEntry {
id: string;
type: "developer" | "group";
label: string;
url?: string;
realms?: (
| "game_developer"
| "client"
| "community_member"
| "customer"
| "staff"
)[];
}
const STORAGE_KEY = "aethex_spotlights";
function loadSpotlights(): SpotlightEntry[] {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return [];
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
function saveSpotlights(entries: SpotlightEntry[]) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
}
export default function AdminSpotlightManager({
profiles,
}: {
profiles: AethexUserProfile[];
}) {
const [entries, setEntries] = useState<SpotlightEntry[]>([]);
const [newGroupName, setNewGroupName] = useState("");
const [newGroupUrl, setNewGroupUrl] = useState("");
const [selectedProfileId, setSelectedProfileId] = useState<string>("");
useEffect(() => {
setEntries(loadSpotlights());
}, []);
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);
if (!profile) return;
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: generateUUID(),
type: "group",
label: newGroupName.trim(),
url: newGroupUrl.trim() || undefined,
},
]);
setNewGroupName("");
setNewGroupUrl("");
};
const move = (index: number, dir: -1 | 1) => {
setEntries((prev) => {
const next = prev.slice();
const j = index + dir;
if (j < 0 || j >= next.length) return prev;
const tmp = next[index];
next[index] = next[j];
next[j] = tmp;
return next;
});
};
const remove = (index: number) => {
setEntries((prev) => prev.filter((_, i) => i !== index));
};
const saveAll = () => saveSpotlights(entries);
return (
<Card className="bg-card/60 border-border/40 backdrop-blur">
<CardHeader>
<div className="flex items-center gap-2">
<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>
</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}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Select a profile" />
</SelectTrigger>
<SelectContent>
{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>
</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)}
/>
</div>
<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>
</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"
>
<div className="flex items-center gap-2">
<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}
</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>
</div>
</li>
))}
{!entries.length && (
<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>
</CardContent>
</Card>
);
}