Add AdminSpotlightManager to manage community spotlights
cgen-b90eba3198e34bfd91668440537fa77a
This commit is contained in:
parent
5336bcf0c2
commit
132f1810af
1 changed files with 151 additions and 0 deletions
151
client/components/admin/AdminSpotlightManager.tsx
Normal file
151
client/components/admin/AdminSpotlightManager.tsx
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
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 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: crypto.randomUUID(), 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue