"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import { createPortal } from "react-dom"; import Image from "next/image"; import Link from "next/link"; import { Loader2, Plus, Trash2, GripVertical, Check, } from "lucide-react"; import { adminFetch } from "@/lib/csrf"; import type { TeamMember } from "@/types/content"; type Member = TeamMember & { id: number }; export default function TeamEditorPage() { const [members, setMembers] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [dragIndex, setDragIndex] = useState(null); const [insertAt, setInsertAt] = useState(null); const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); const [dragSize, setDragSize] = useState({ w: 0, h: 0 }); const [grabOffset, setGrabOffset] = useState({ x: 0, y: 0 }); const itemRefs = useRef<(HTMLDivElement | null)[]>([]); useEffect(() => { adminFetch("/api/admin/team") .then((r) => r.json()) .then(setMembers) .finally(() => setLoading(false)); }, []); const saveOrder = useCallback(async (updated: Member[]) => { setMembers(updated); setSaving(true); await adminFetch("/api/admin/team/reorder", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids: updated.map((m) => m.id) }), }); setSaving(false); setSaved(true); setTimeout(() => setSaved(false), 2000); }, []); const startDrag = useCallback( (clientX: number, clientY: number, index: number) => { const el = itemRefs.current[index]; if (!el) return; const rect = el.getBoundingClientRect(); setDragIndex(index); setInsertAt(index); setMousePos({ x: clientX, y: clientY }); setDragSize({ w: rect.width, h: rect.height }); setGrabOffset({ x: clientX - rect.left, y: clientY - rect.top }); }, [] ); const handleGripMouseDown = useCallback( (e: React.MouseEvent, index: number) => { e.preventDefault(); startDrag(e.clientX, e.clientY, index); }, [startDrag] ); const handleCardMouseDown = useCallback( (e: React.MouseEvent, index: number) => { const tag = (e.target as HTMLElement).closest("input, textarea, select, button, a, [role='switch']"); if (tag) return; e.preventDefault(); const x = e.clientX; const y = e.clientY; const pendingIndex = index; let moved = false; function onMove(ev: MouseEvent) { const dx = ev.clientX - x; const dy = ev.clientY - y; if (Math.abs(dx) > 8 || Math.abs(dy) > 8) { moved = true; cleanup(); startDrag(ev.clientX, ev.clientY, pendingIndex); } } function onUp() { cleanup(); if (!moved) { window.location.href = `/admin/team/${members[pendingIndex].id}`; } } function cleanup() { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); } window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onUp); }, [startDrag, members] ); useEffect(() => { if (dragIndex === null) return; document.body.style.userSelect = "none"; function onMouseMove(e: MouseEvent) { setMousePos({ x: e.clientX, y: e.clientY }); let newInsert = members.length; for (let i = 0; i < members.length; i++) { if (i === dragIndex) continue; const el = itemRefs.current[i]; if (!el) continue; const rect = el.getBoundingClientRect(); const midY = rect.top + rect.height / 2; if (e.clientY < midY) { newInsert = i > dragIndex! ? i : i; break; } } setInsertAt(newInsert); } function onMouseUp() { setDragIndex((prevDrag) => { setInsertAt((prevInsert) => { if (prevDrag !== null && prevInsert !== null) { let targetIndex = prevInsert; if (prevDrag < targetIndex) targetIndex -= 1; if (prevDrag !== targetIndex) { const updated = [...members]; const [moved] = updated.splice(prevDrag, 1); updated.splice(targetIndex, 0, moved); saveOrder(updated); } } return null; }); return null; }); } window.addEventListener("mousemove", onMouseMove); window.addEventListener("mouseup", onMouseUp); return () => { document.body.style.userSelect = ""; window.removeEventListener("mousemove", onMouseMove); window.removeEventListener("mouseup", onMouseUp); }; }, [dragIndex, members, saveOrder]); async function deleteMember(id: number) { if (!confirm("Удалить этого участника?")) return; await adminFetch(`/api/admin/team/${id}`, { method: "DELETE" }); setMembers((prev) => prev.filter((m) => m.id !== id)); } if (loading) { return (
Загрузка...
); } const draggedMember = dragIndex !== null ? members[dragIndex] : null; // Build the visual order: remove dragged item, insert placeholder at insertAt function renderList() { if (dragIndex === null || insertAt === null) { // Normal render — no drag return members.map((member, i) => (
{ itemRefs.current[i] = el; }} onMouseDown={(e) => handleCardMouseDown(e, i)} className="flex items-center gap-4 rounded-lg border border-white/10 bg-neutral-900/50 p-3 mb-2 hover:border-white/25 hover:bg-neutral-800/50 transition-colors cursor-pointer" >
handleGripMouseDown(e, i)} >
{member.name}

{member.name}

{member.role}

)); } // During drag: build list without the dragged item, with placeholder inserted const elements: React.ReactNode[] = []; let visualIndex = 0; // Determine where to insert placeholder relative to non-dragged items let placeholderPos = insertAt; if (insertAt > dragIndex) placeholderPos = insertAt - 1; for (let i = 0; i < members.length; i++) { if (i === dragIndex) { // Keep a hidden ref so midpoint detection still works elements.push(
{ itemRefs.current[i] = el; }} className="hidden" /> ); continue; } if (visualIndex === placeholderPos) { elements.push(
); } const member = members[i]; elements.push(
{ itemRefs.current[i] = el; }} onMouseDown={(e) => handleCardMouseDown(e, i)} className="flex items-center gap-4 rounded-lg border border-white/10 bg-neutral-900/50 p-3 mb-2 hover:border-white/25 hover:bg-neutral-800/50 transition-colors cursor-pointer" >
handleGripMouseDown(e, i)} >
{member.name}

{member.name}

{member.role}

); visualIndex++; } // Placeholder at the end if (visualIndex === placeholderPos) { elements.push(
); } return elements; } return (

Команда

{(saving || saved) && ( {saving ? ( ) : ( )} {saving ? "Сохранение..." : "Сохранено!"} )} Добавить
{renderList()}
{/* Floating card following cursor */} {dragIndex !== null && draggedMember && createPortal(
{draggedMember.name}

{draggedMember.name}

{draggedMember.role}

, document.body )}
); }