"use client"; import { useState, useEffect, useRef, useCallback } from "react"; import { useRouter, useParams } from "next/navigation"; import Image from "next/image"; import { Save, Loader2, Check, ArrowLeft, Upload, AlertCircle } from "lucide-react"; import { InputField, TextareaField, ListField, VictoryListField, VictoryItemListField } from "../../_components/FormField"; import { adminFetch } from "@/lib/csrf"; import type { RichListItem, VictoryItem } from "@/types/content"; function extractUsername(value: string): string { if (!value) return ""; // Strip full URL → username const cleaned = value.replace(/^https?:\/\/(www\.)?instagram\.com\//, "").replace(/\/$/, "").replace(/^@/, ""); return cleaned; } interface MemberForm { name: string; role: string; image: string; instagram: string; description: string; experience: string[]; victories: VictoryItem[]; education: RichListItem[]; } export default function TeamMemberEditorPage() { const router = useRouter(); const { id } = useParams<{ id: string }>(); const isNew = id === "new"; const [data, setData] = useState({ name: "", role: "", image: "/images/team/placeholder.webp", instagram: "", description: "", experience: [], victories: [], education: [], }); const [loading, setLoading] = useState(!isNew); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [uploading, setUploading] = useState(false); // Instagram validation const [igStatus, setIgStatus] = useState<"idle" | "checking" | "valid" | "invalid">("idle"); const igTimerRef = useRef | null>(null); const validateInstagram = useCallback((username: string) => { if (igTimerRef.current) clearTimeout(igTimerRef.current); if (!username) { setIgStatus("idle"); return; } setIgStatus("checking"); igTimerRef.current = setTimeout(async () => { try { const res = await adminFetch(`/api/admin/validate-instagram?username=${encodeURIComponent(username)}`); const result = await res.json(); setIgStatus(result.valid ? "valid" : "invalid"); } catch { setIgStatus("idle"); } }, 800); }, []); // Link validation for bio const [linkErrors, setLinkErrors] = useState>({}); function validateUrl(url: string): boolean { if (!url) return true; try { new URL(url); return true; } catch { return false; } } // City validation for victories const [cityErrors, setCityErrors] = useState>({}); const [citySuggestions, setCitySuggestions] = useState<{ index: number; items: string[] } | null>(null); const cityTimerRef = useRef | null>(null); const searchCity = useCallback((index: number, query: string) => { if (cityTimerRef.current) clearTimeout(cityTimerRef.current); if (!query || query.length < 2) { setCitySuggestions(null); return; } cityTimerRef.current = setTimeout(async () => { try { const res = await fetch( `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&addressdetails=1&limit=5&accept-language=ru`, { headers: { "User-Agent": "BlackheartAdmin/1.0" } } ); const results = await res.json(); const cities = results .map((r: Record) => { const addr = r.address as Record | undefined; const city = addr?.city || addr?.town || addr?.village || addr?.state || (r.name as string); const country = addr?.country || ""; return country ? `${city}, ${country}` : city; }) .filter((v: string, i: number, a: string[]) => a.indexOf(v) === i) .slice(0, 6); setCitySuggestions(cities.length > 0 ? { index, items: cities } : null); setCityErrors((prev) => { const n = { ...prev }; delete n[index]; return n; }); } catch { setCitySuggestions(null); } }, 500); }, []); useEffect(() => { if (isNew) return; adminFetch(`/api/admin/team/${id}`) .then((r) => r.json()) .then((member) => { const username = extractUsername(member.instagram || ""); setData({ name: member.name, role: member.role, image: member.image, instagram: username, description: member.description || "", experience: member.experience || [], victories: member.victories || [], education: member.education || [], }); if (username) setIgStatus("valid"); // existing data is trusted }) .finally(() => setLoading(false)); }, [id, isNew]); const hasErrors = igStatus === "invalid" || Object.keys(linkErrors).length > 0 || Object.keys(cityErrors).length > 0; async function handleSave() { if (hasErrors) return; setSaving(true); setSaved(false); // Build instagram as full URL for storage if username is provided const payload = { ...data, instagram: data.instagram ? `https://instagram.com/${data.instagram}` : "", }; if (isNew) { const res = await adminFetch("/api/admin/team", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (res.ok) { router.push("/admin/team"); } } else { const res = await adminFetch(`/api/admin/team/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (res.ok) { setSaved(true); setTimeout(() => setSaved(false), 2000); } } setSaving(false); } async function handleUpload(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (!file) return; setUploading(true); const formData = new FormData(); formData.append("file", file); formData.append("folder", "team"); try { const res = await adminFetch("/api/admin/upload", { method: "POST", body: formData, }); const result = await res.json(); if (result.path) { setData((prev) => ({ ...prev, image: result.path })); } } catch { // Upload failed silently } finally { setUploading(false); } } if (loading) { return (
Загрузка...
); } return (

{isNew ? "Новый участник" : data.name}

{/* Photo */}

Фото

{data.name
{/* Fields */}
setData({ ...data, name: v })} /> setData({ ...data, role: v })} />
@ { const username = extractUsername(e.target.value); setData({ ...data, instagram: username }); validateInstagram(username); }} placeholder="username" className={`w-full rounded-lg border bg-neutral-800 pl-8 pr-10 py-2.5 text-white placeholder-neutral-500 outline-none transition-colors ${ igStatus === "invalid" ? "border-red-500 focus:border-red-500" : igStatus === "valid" ? "border-green-500/50 focus:border-green-500" : "border-white/10 focus:border-gold" }`} /> {igStatus === "checking" && } {igStatus === "valid" && } {igStatus === "invalid" && }
{igStatus === "invalid" && (

Аккаунт не найден

)} {data.instagram && igStatus !== "invalid" && (

instagram.com/{data.instagram}

)}
setData({ ...data, description: v })} rows={6} />

Биография

setData({ ...data, experience: items })} placeholder="Например: 10 лет в танцах" /> setData({ ...data, victories: items })} cityErrors={cityErrors} citySuggestions={citySuggestions} onCitySearch={searchCity} onCitySelect={(i, v) => { const updated = data.victories.map((item, idx) => idx === i ? { ...item, location: v } : item); setData({ ...data, victories: updated }); setCitySuggestions(null); setCityErrors((prev) => { const n = { ...prev }; delete n[i]; return n; }); }} onLinkValidate={(key, error) => { setLinkErrors((prev) => { if (error) return { ...prev, [key]: error }; const n = { ...prev }; delete n[key]; return n; }); }} /> setData({ ...data, education: items })} placeholder="Например: Сертификат IPSF" onLinkValidate={(key, error) => { setLinkErrors((prev) => { if (error) return { ...prev, [key]: error }; const n = { ...prev }; delete n[key]; return n; }); }} />
); }