feat: upgrade team admin with click-to-edit, Instagram validation, date picker, city autocomplete

- Team list: click card to open editor (remove pencil button), keep drag-to-reorder
- Instagram field: username-only input with @ prefix, async account validation via HEAD request
- Victory dates: date range picker replacing text input, auto-formats to DD.MM.YYYY / DD-DD.MM.YYYY
- Victory location: city autocomplete via Nominatim API with suggestions dropdown
- Links: real-time URL validation with error indicators on all link fields
- Save button blocked when any validation errors exist

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 15:34:55 +03:00
parent 4918184852
commit 627781027b
5 changed files with 561 additions and 223 deletions

View File

@@ -9,7 +9,6 @@ import {
Plus,
Trash2,
GripVertical,
Pencil,
Check,
} from "lucide-react";
import type { TeamMember } from "@/types/content";
@@ -80,16 +79,23 @@ export default function TeamEditorPage() {
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(); }
function onUp() {
cleanup();
if (!moved) {
window.location.href = `/admin/team/${members[pendingIndex].id}`;
}
}
function cleanup() {
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseup", onUp);
@@ -97,7 +103,7 @@ export default function TeamEditorPage() {
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onUp);
},
[startDrag]
[startDrag, members]
);
useEffect(() => {
@@ -177,7 +183,7 @@ export default function TeamEditorPage() {
key={member.id}
ref={(el) => { 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"
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"
>
<div
className="cursor-grab active:cursor-grabbing text-neutral-500 hover:text-white transition-colors select-none"
@@ -192,14 +198,9 @@ export default function TeamEditorPage() {
<p className="font-medium text-white truncate">{member.name}</p>
<p className="text-sm text-neutral-400 truncate">{member.role}</p>
</div>
<div className="flex items-center gap-1">
<Link href={`/admin/team/${member.id}`} className="rounded p-2 text-neutral-400 hover:text-white transition-colors">
<Pencil size={16} />
</Link>
<button onClick={() => deleteMember(member.id)} className="rounded p-2 text-neutral-400 hover:text-red-400 transition-colors">
<Trash2 size={16} />
</button>
</div>
<button onClick={(e) => { e.stopPropagation(); deleteMember(member.id); }} className="rounded p-2 text-neutral-400 hover:text-red-400 transition-colors">
<Trash2 size={16} />
</button>
</div>
));
}
@@ -237,7 +238,7 @@ export default function TeamEditorPage() {
key={member.id}
ref={(el) => { 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"
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"
>
<div
className="cursor-grab active:cursor-grabbing text-neutral-500 hover:text-white transition-colors select-none"
@@ -252,14 +253,9 @@ export default function TeamEditorPage() {
<p className="font-medium text-white truncate">{member.name}</p>
<p className="text-sm text-neutral-400 truncate">{member.role}</p>
</div>
<div className="flex items-center gap-1">
<Link href={`/admin/team/${member.id}`} className="rounded p-2 text-neutral-400 hover:text-white transition-colors">
<Pencil size={16} />
</Link>
<button onClick={() => deleteMember(member.id)} className="rounded p-2 text-neutral-400 hover:text-red-400 transition-colors">
<Trash2 size={16} />
</button>
</div>
<button onClick={(e) => { e.stopPropagation(); deleteMember(member.id); }} className="rounded p-2 text-neutral-400 hover:text-red-400 transition-colors">
<Trash2 size={16} />
</button>
</div>
);
visualIndex++;