"use client"; export const dynamic = "force-dynamic"; import { useState, useEffect, useCallback, useRef } from "react"; import Link from "next/link"; import { ArrowLeft, Users, Plus, Trash2, Loader2, GripVertical, Eye, EyeOff, Sparkles, Upload, Check, Linkedin, Mail, Globe, Twitter, ChevronDown, } from "lucide-react"; import { getTeamMembers, createTeamMember, updateTeamMember, deleteTeamMember, reorderTeamMembers, } from "./actions"; import { useHqUi } from "@/components/hq/Toast"; interface MemberRow { id: string; name: string; role: string; bio: string | null; photoUrl: string | null; email: string | null; linkedinUrl: string | null; xUrl: string | null; websiteUrl: string | null; isActive: boolean; order: number; translationsJson: string | null; } export default function TeamDashboard() { const ui = useHqUi(); const [members, setMembers] = useState([]); const [loading, setLoading] = useState(true); const [expandedId, setExpandedId] = useState(null); const [savingId, setSavingId] = useState(null); const [savedFlash, setSavedFlash] = useState(null); const [draggedId, setDraggedId] = useState(null); const [creating, setCreating] = useState(false); const load = useCallback(async () => { setLoading(true); const res = await getTeamMembers(); if (res.success && res.members) setMembers(res.members as MemberRow[]); setLoading(false); }, []); useEffect(() => { load(); }, [load]); const flashSaved = (id: string) => { setSavedFlash(id); setTimeout(() => setSavedFlash(null), 1500); }; const handleAdd = async () => { setCreating(true); const res = await createTeamMember({ name: "New member", role: "Role / Title" }); setCreating(false); if (res.success && res.member) { await load(); setExpandedId(res.member.id); } else { ui.toast(res.error || "Could not create member", "error"); } }; // Inline patch with optimistic update + auto-save (name/role/isActive). const patch = async (id: string, p: Partial) => { setMembers((prev) => prev.map((m) => (m.id === id ? { ...m, ...p } : m))); setSavingId(id); const res = await updateTeamMember(id, p as never); setSavingId(null); if (res.success) flashSaved(id); else ui.toast(res.error || "Save failed", "error"); }; const handleDelete = async (id: string, name: string) => { const ok = await ui.confirm({ title: "Remove team member", message: `Remove ${name} from the public team page. This cannot be undone.`, confirmLabel: "Remove", destructive: true, }); if (!ok) return; await deleteTeamMember(id); ui.toast("Member removed.", "success"); await load(); }; // Drag reorder — same pattern as the Hero panel. const onDrop = async (targetId: string) => { if (!draggedId || draggedId === targetId) return; const ids = members.map((m) => m.id); const from = ids.indexOf(draggedId); const to = ids.indexOf(targetId); if (from < 0 || to < 0) return; const reordered = [...ids]; reordered.splice(from, 1); reordered.splice(to, 0, draggedId); setMembers((prev) => reordered.map((id, i) => ({ ...prev.find((m) => m.id === id)!, order: i }))); setDraggedId(null); await reorderTeamMembers(reordered); }; return (
Back to Dashboard
The Team

Team Members.

Drag to reorder. Click a card to edit photo, bio and social links. Name & role auto-save.

{loading ? (
Loading team…
) : members.length === 0 ? (

No team members yet.

Click “Add member” to build the public team page.

) : (
{members.map((m) => { const isExpanded = expandedId === m.id; const isSaving = savingId === m.id; const justSaved = savedFlash === m.id; return (
setDraggedId(m.id)} onDragOver={(e) => e.preventDefault()} onDrop={() => onDrop(m.id)} className={`bg-[#111] border rounded-2xl overflow-hidden transition-all ${ draggedId === m.id ? "opacity-50" : "" } ${m.isActive ? "border-white/10" : "border-white/5 opacity-60"}`} >
{m.photoUrl ? ( // eslint-disable-next-line @next/next/no-img-element {m.name} ) : (
)}
setMembers((prev) => prev.map((x) => (x.id === m.id ? { ...x, name: e.target.value } : x)))} onBlur={(e) => patch(m.id, { name: e.target.value })} placeholder="Full name" className="w-full bg-transparent border-0 outline-none text-white text-sm font-medium placeholder:text-[#86868B] focus:bg-white/[0.04] rounded px-2 py-1 -mx-2" /> setMembers((prev) => prev.map((x) => (x.id === m.id ? { ...x, role: e.target.value } : x)))} onBlur={(e) => patch(m.id, { role: e.target.value })} placeholder="Role / title" className="w-full bg-transparent border-0 outline-none text-[#00F0FF] text-xs placeholder:text-[#86868B] focus:bg-white/[0.04] rounded px-2 py-0.5 -mx-2 mt-0.5" />
{isSaving && } {justSaved && Saved}
{isExpanded && ( { await load(); }} /> )}
); })}
)}
); } // ─── Expanded editor: photo upload + bio + social links + AI translate ─────── function MemberEditor({ member, onSaved }: { member: MemberRow; onSaved: () => Promise }) { const ui = useHqUi(); const [photoUrl, setPhotoUrl] = useState(member.photoUrl || ""); const [bio, setBio] = useState(member.bio || ""); const [email, setEmail] = useState(member.email || ""); const [linkedinUrl, setLinkedinUrl] = useState(member.linkedinUrl || ""); const [xUrl, setXUrl] = useState(member.xUrl || ""); const [websiteUrl, setWebsiteUrl] = useState(member.websiteUrl || ""); const [autoTranslate, setAutoTranslate] = useState(false); const [saving, setSaving] = useState(false); const [uploading, setUploading] = useState(false); const fileRef = useRef(null); const uploadPhoto = async (file: File) => { setUploading(true); try { const fd = new FormData(); fd.append("scope", "team"); fd.append("optimize", "1"); fd.append("file", file); const res = await fetch("/api/assets", { method: "POST", body: fd }); const data = await res.json(); if (data.success) setPhotoUrl(data.file.publicUrl); else ui.toast(data.error || "Upload failed", "error"); } catch (err: unknown) { ui.toast(err instanceof Error ? err.message : "Upload failed", "error"); } setUploading(false); }; const save = async () => { setSaving(true); const res = await updateTeamMember(member.id, { bio, photoUrl, email, linkedinUrl, xUrl, websiteUrl, autoTranslate, }); setSaving(false); if (res.success) { ui.toast("Saved.", "success"); await onSaved(); } else ui.toast(res.error || "Save failed", "error"); }; return (
{/* Photo */}
{photoUrl ? ( // eslint-disable-next-line @next/next/no-img-element ) : (
)}
{ const f = e.target.files?.[0]; if (f) uploadPhoto(f); e.target.value = ""; }} />

Square portrait recommended, min 400×400.

{/* Bio */}