"use client"; import { useState, useRef, useEffect, useMemo } from "react"; import { SectionEditor } from "../_components/SectionEditor"; import { InputField, TextareaField, ParticipantLimits, AutocompleteMulti } from "../_components/FormField"; import { ArrayEditor } from "../_components/ArrayEditor"; import { Plus, X, Upload, Loader2, ImageIcon, AlertCircle, Check } from "lucide-react"; import { adminFetch } from "@/lib/csrf"; import type { MasterClassItem, MasterClassSlot } from "@/types/content"; function PriceField({ label, value, onChange, placeholder }: { label: string; value: string; onChange: (v: string) => void; placeholder?: string }) { const raw = value.replace(/\s*BYN\s*$/i, "").trim(); return (
{ const v = e.target.value; onChange(v ? `${v} BYN` : ""); }} placeholder={placeholder ?? "0"} className="flex-1 bg-transparent px-4 py-2.5 text-white placeholder-neutral-500 outline-none min-w-0" /> BYN
); } interface MasterClassesData { title: string; items: MasterClassItem[]; } // --- Location Select --- function LocationSelect({ value, onChange, locations, }: { value: string; onChange: (v: string) => void; locations: { name: string; address: string }[]; }) { return (
{locations.map((loc) => { const active = value === loc.name; return ( ); })}
); } // --- Date List --- function calcDurationText(startTime: string, endTime: string): string { if (!startTime || !endTime) return ""; const [sh, sm] = startTime.split(":").map(Number); const [eh, em] = endTime.split(":").map(Number); const mins = (eh * 60 + em) - (sh * 60 + sm); if (mins <= 0) return ""; const h = Math.floor(mins / 60); const m = mins % 60; if (h > 0 && m > 0) return `${h} ч ${m} мин`; if (h > 0) return h === 1 ? "1 час" : h < 5 ? `${h} часа` : `${h} часов`; return `${m} мин`; } function SlotsField({ slots, onChange, }: { slots: MasterClassSlot[]; onChange: (slots: MasterClassSlot[]) => void; }) { function addSlot() { // Copy time from last slot for convenience const last = slots[slots.length - 1]; onChange([...slots, { date: "", startTime: last?.startTime ?? "", endTime: last?.endTime ?? "", }]); } function updateSlot(index: number, patch: Partial) { onChange(slots.map((s, i) => (i === index ? { ...s, ...patch } : s))); } function removeSlot(index: number) { onChange(slots.filter((_, i) => i !== index)); } return (
{slots.map((slot, i) => { const dur = calcDurationText(slot.startTime, slot.endTime); return (
updateSlot(i, { date: e.target.value })} className={`w-[140px] rounded-lg border bg-neutral-800 px-3 py-2 text-sm text-white outline-none transition-colors [color-scheme:dark] ${ !slot.date ? "border-red-500/50" : "border-white/10 focus:border-gold" }`} /> updateSlot(i, { startTime: e.target.value })} className="w-[100px] rounded-lg border border-white/10 bg-neutral-800 px-3 py-2 text-sm text-white outline-none focus:border-gold transition-colors [color-scheme:dark]" /> updateSlot(i, { endTime: e.target.value })} className="w-[100px] rounded-lg border border-white/10 bg-neutral-800 px-3 py-2 text-sm text-white outline-none focus:border-gold transition-colors [color-scheme:dark]" /> {dur && ( {dur} )}
); })}
); } // --- Image Upload --- function ImageUploadField({ value, onChange, }: { value: string; onChange: (path: string) => void; }) { const [uploading, setUploading] = useState(false); const inputRef = useRef(null); 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", "master-classes"); try { const res = await adminFetch("/api/admin/upload", { method: "POST", body: formData, }); const result = await res.json(); if (result.path) onChange(result.path); } catch { /* upload failed */ } finally { setUploading(false); } } return (
{value ? (
{value.split("/").pop()}
) : ( )}
); } // --- Instagram Link Field --- function InstagramLinkField({ value, onChange, }: { value: string; onChange: (v: string) => void; }) { const error = getInstagramError(value); return (
onChange(e.target.value)} placeholder="https://instagram.com/p/... или /reel/..." className={`w-full rounded-lg border bg-neutral-800 px-4 py-2.5 text-white placeholder-neutral-500 outline-none transition-colors ${ error ? "border-red-500/50" : "border-white/10 focus:border-gold" }`} /> {value && !error && ( )} {error && ( )}
{error && (

{error}

)}
); } function getInstagramError(url: string): string | null { if (!url) return null; try { const parsed = new URL(url); const host = parsed.hostname.replace("www.", ""); if (host !== "instagram.com" && host !== "instagr.am") { return "Ссылка должна вести на instagram.com"; } const validPaths = ["/p/", "/reel/", "/tv/", "/stories/"]; if (!validPaths.some((p) => parsed.pathname.includes(p))) { return "Ожидается ссылка на пост, рилс или сторис (/p/, /reel/, /tv/)"; } return null; } catch { return "Некорректная ссылка"; } } // --- Validation badge --- function ValidationHint({ fields }: { fields: Record }) { const missing = Object.entries(fields).filter(([, v]) => !(v ?? "").trim()); if (missing.length === 0) return null; return (
Не заполнено: {missing.map(([k]) => k).join(", ")}
); } // --- Main page --- export default function MasterClassesEditorPage() { const [trainers, setTrainers] = useState([]); const [styles, setStyles] = useState([]); const [locations, setLocations] = useState<{ name: string; address: string }[]>([]); useEffect(() => { // Fetch trainers from team adminFetch("/api/admin/team") .then((r) => r.json()) .then((members: { name: string }[]) => { setTrainers(members.map((m) => m.name)); }) .catch(() => {}); // Fetch styles from classes section adminFetch("/api/admin/sections/classes") .then((r) => r.json()) .then((data: { items: { name: string }[] }) => { setStyles(data.items.map((c) => c.name)); }) .catch(() => {}); // Fetch locations from schedule section adminFetch("/api/admin/sections/schedule") .then((r) => r.json()) .then((data: { locations: { name: string; address: string }[] }) => { setLocations(data.locations); }) .catch(() => {}); }, []); return ( sectionKey="masterClasses" title="Мастер-классы" > {(data, update) => ( <> update({ ...data, title: v })} /> update({ ...data, items })} renderItem={(item, _i, updateItem) => (
0 ? "ok" : "", }} /> updateItem({ ...item, title: v })} placeholder="Мастер-класс от Анны Тарыбы" /> updateItem({ ...item, image: v })} />
updateItem({ ...item, trainer: v })} options={trainers} placeholder="Добавить тренера..." /> updateItem({ ...item, style: v })} options={styles} placeholder="Добавить стиль..." />
updateItem({ ...item, cost: v })} placeholder="40" /> {locations.length > 0 && ( updateItem({ ...item, location: v || undefined }) } locations={locations} /> )} updateItem({ ...item, slots })} /> updateItem({ ...item, description: v || undefined }) } placeholder="Описание мастер-класса, трек, стиль..." rows={3} /> updateItem({ ...item, instagramUrl: v || undefined }) } /> updateItem({ ...item, minParticipants: v })} onMaxChange={(v) => updateItem({ ...item, maxParticipants: v })} />
)} createItem={() => ({ title: "", image: "", slots: [], trainer: "", cost: "", style: "", })} addLabel="Добавить мастер-класс" /> )} ); }