refactor: compact searchable SelectField for Open Day, removed time field from slot editor
This commit is contained in:
@@ -191,7 +191,7 @@ export function SelectField({
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<label className="block text-sm text-neutral-400 mb-1.5">{label}</label>
|
||||
{label && <label className="block text-sm text-neutral-400 mb-1.5">{label}</label>}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -199,9 +199,9 @@ export function SelectField({
|
||||
setSearch("");
|
||||
setTimeout(() => inputRef.current?.focus(), 0);
|
||||
}}
|
||||
className={`w-full rounded-lg border bg-neutral-800 px-4 py-2.5 text-left outline-none transition-colors ${
|
||||
open ? "border-gold" : "border-white/10"
|
||||
} ${value ? "text-white" : "text-neutral-500"}`}
|
||||
className={`w-full rounded-lg border bg-neutral-800 text-left outline-none transition-colors ${
|
||||
label ? "px-4 py-2.5" : "px-2 py-1 text-xs"
|
||||
} ${open ? "border-gold" : "border-white/10"} ${value ? "text-white" : "text-neutral-500"}`}
|
||||
>
|
||||
{selectedLabel || placeholder || "Выберите..."}
|
||||
</button>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Plus, X, Loader2, Calendar, Trash2, Ban, CheckCircle2, RotateCcw, Sparkles,
|
||||
} from "lucide-react";
|
||||
import { adminFetch } from "@/lib/csrf";
|
||||
import { ParticipantLimits } from "../_components/FormField";
|
||||
import { ParticipantLimits, SelectField } from "../_components/FormField";
|
||||
|
||||
// --- Types ---
|
||||
|
||||
@@ -179,6 +179,57 @@ function EventSettings({
|
||||
);
|
||||
}
|
||||
|
||||
// --- New Class Form (create only on save) ---
|
||||
|
||||
function NewClassForm({
|
||||
startTime,
|
||||
trainers,
|
||||
styles,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: {
|
||||
startTime: string;
|
||||
trainers: string[];
|
||||
styles: string[];
|
||||
onSave: (data: { trainer: string; style: string; endTime: string }) => void;
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const [style, setStyle] = useState("");
|
||||
const [trainer, setTrainer] = useState("");
|
||||
const endTime = addHour(startTime);
|
||||
const formRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
formRef.current?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}, []);
|
||||
|
||||
// Auto-save on click outside
|
||||
useEffect(() => {
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
if (formRef.current && !formRef.current.contains(e.target as Node)) {
|
||||
if (style && trainer) onSave({ trainer, style, endTime });
|
||||
else onCancel();
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [style, trainer, endTime, onSave, onCancel]);
|
||||
|
||||
const canSave = style && trainer;
|
||||
|
||||
return (
|
||||
<div ref={formRef} className="p-2 space-y-1.5 ring-1 ring-gold/30 rounded-lg">
|
||||
<SelectField label="" value={style} onChange={setStyle} options={styles.map((s) => ({ value: s, label: s }))} placeholder="Стиль..." />
|
||||
<SelectField label="" value={trainer} onChange={setTrainer} options={trainers.map((t) => ({ value: t, label: t }))} placeholder="Тренер..." />
|
||||
<div className="flex gap-1 justify-end">
|
||||
<button onClick={onCancel} className="text-[10px] text-neutral-500 hover:text-white px-1">Отмена</button>
|
||||
<button onClick={() => canSave && onSave({ trainer, style, endTime })} disabled={!canSave}
|
||||
className="text-[10px] text-gold hover:text-gold-light px-1 font-medium disabled:opacity-30 disabled:cursor-not-allowed">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Class Grid Cell ---
|
||||
|
||||
function ClassCell({
|
||||
@@ -186,7 +237,6 @@ function ClassCell({
|
||||
minBookings,
|
||||
trainers,
|
||||
styles,
|
||||
autoEdit,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
onCancel,
|
||||
@@ -195,49 +245,28 @@ function ClassCell({
|
||||
minBookings: number;
|
||||
trainers: string[];
|
||||
styles: string[];
|
||||
autoEdit?: boolean;
|
||||
onUpdate: (id: number, data: Partial<OpenDayClass>) => void;
|
||||
onDelete: (id: number) => void;
|
||||
onCancel: (id: number) => void;
|
||||
}) {
|
||||
const [editing, setEditing] = useState(!!autoEdit);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [trainer, setTrainer] = useState(cls.trainer);
|
||||
const [style, setStyle] = useState(cls.style);
|
||||
const cellRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoEdit && cellRef.current) {
|
||||
cellRef.current.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
}, [autoEdit]);
|
||||
const [endTime, setEndTime] = useState(cls.endTime);
|
||||
|
||||
const atRisk = cls.bookingCount < minBookings && !cls.cancelled;
|
||||
|
||||
function save() {
|
||||
if (trainer.trim() && style.trim()) {
|
||||
onUpdate(cls.id, { trainer: trainer.trim(), style: style.trim(), endTime });
|
||||
onUpdate(cls.id, { trainer: trainer.trim(), style: style.trim() });
|
||||
setEditing(false);
|
||||
}
|
||||
}
|
||||
|
||||
const selectCls = "w-full rounded-md border border-white/10 bg-neutral-800 px-2 py-1 text-xs text-white outline-none focus:border-gold [color-scheme:dark]";
|
||||
|
||||
if (editing) {
|
||||
return (
|
||||
<div ref={cellRef} className="p-2 space-y-1.5 ring-1 ring-gold/30 rounded-lg">
|
||||
<select value={style} onChange={(e) => setStyle(e.target.value)} className={selectCls}>
|
||||
<option value="">Стиль...</option>
|
||||
{styles.map((s) => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
<select value={trainer} onChange={(e) => setTrainer(e.target.value)} className={selectCls}>
|
||||
<option value="">Тренер...</option>
|
||||
{trainers.map((t) => <option key={t} value={t}>{t}</option>)}
|
||||
</select>
|
||||
<div>
|
||||
<label className="text-[10px] text-neutral-500 mb-0.5 block">До</label>
|
||||
<input type="time" value={endTime} onChange={(e) => setEndTime(e.target.value)} className={selectCls} />
|
||||
</div>
|
||||
<div className="p-2 space-y-1.5 rounded-lg">
|
||||
<SelectField label="" value={style} onChange={setStyle} options={styles.map((s) => ({ value: s, label: s }))} placeholder="Стиль..." />
|
||||
<SelectField label="" value={trainer} onChange={setTrainer} options={trainers.map((t) => ({ value: t, label: t }))} placeholder="Тренер..." />
|
||||
<div className="flex gap-1 justify-end">
|
||||
<button onClick={() => setEditing(false)} className="text-[10px] text-neutral-500 hover:text-white px-1">
|
||||
Отмена
|
||||
@@ -341,16 +370,15 @@ function ScheduleGrid({
|
||||
return counts;
|
||||
}, [classes, halls]);
|
||||
|
||||
const [newClassTime, setNewClassTime] = useState<string | null>(null);
|
||||
const [creatingTime, setCreatingTime] = useState<string | null>(null);
|
||||
|
||||
async function addClass(startTime: string) {
|
||||
const endTime = addHour(startTime);
|
||||
async function confirmCreate(startTime: string, data: { trainer: string; style: string; endTime: string }) {
|
||||
await adminFetch("/api/admin/open-day/classes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ eventId, hall: selectedHall, startTime, endTime, trainer: "—", style: "—" }),
|
||||
body: JSON.stringify({ eventId, hall: selectedHall, startTime, endTime: data.endTime, trainer: data.trainer, style: data.style }),
|
||||
});
|
||||
setNewClassTime(startTime);
|
||||
setCreatingTime(null);
|
||||
onClassesChange();
|
||||
}
|
||||
|
||||
@@ -418,14 +446,21 @@ function ScheduleGrid({
|
||||
minBookings={minBookings}
|
||||
trainers={trainers}
|
||||
styles={styles}
|
||||
autoEdit={cls.startTime === newClassTime}
|
||||
onUpdate={(id, data) => { setNewClassTime(null); updateClass(id, data); }}
|
||||
onUpdate={updateClass}
|
||||
onDelete={deleteClass}
|
||||
onCancel={cancelClass}
|
||||
/>
|
||||
) : creatingTime === time ? (
|
||||
<NewClassForm
|
||||
startTime={time}
|
||||
trainers={trainers}
|
||||
styles={styles}
|
||||
onSave={(data) => confirmCreate(time, data)}
|
||||
onCancel={() => setCreatingTime(null)}
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => addClass(time)}
|
||||
onClick={() => setCreatingTime(time)}
|
||||
className="w-full rounded-lg border border-dashed border-white/5 p-2 text-neutral-600 hover:text-gold hover:border-gold/20 transition-colors"
|
||||
>
|
||||
<Plus size={12} className="mx-auto" />
|
||||
|
||||
Reference in New Issue
Block a user