feat: booking UX improvements — waiting list, card focus, sort order
- Auto-note "Лист ожидания" for registrations when class is full - Waiting list triggers on confirmed count (not total registrations) - Card highlight + scroll after status change - Hover effect on booking cards - Freshly changed cards appear first in their status group - Polling no longer remounts tabs (fixes page jump on approve) - Fix MasterClassesData missing waitingListText type - Add Turbopack troubleshooting docs to CLAUDE.md
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { useState, useMemo, useRef, useEffect, useCallback } from "react";
|
||||
import { ChevronDown, ChevronRight, Archive } from "lucide-react";
|
||||
import { adminFetch } from "@/lib/csrf";
|
||||
import { type BookingStatus, type BookingFilter, type BaseBooking, type BookingGroup, sortByStatus } from "./types";
|
||||
@@ -32,8 +32,20 @@ export function GenericBookingsList<T extends BaseBooking>({
|
||||
}: GenericBookingsListProps<T>) {
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
||||
const [highlightId, setHighlightId] = useState<number | null>(null);
|
||||
const highlightRef = useRef<HTMLDivElement>(null);
|
||||
const { showError } = useToast();
|
||||
|
||||
// Scroll to highlighted card and clear highlight after animation
|
||||
useEffect(() => {
|
||||
if (highlightId === null) return;
|
||||
const timer = setTimeout(() => {
|
||||
highlightRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||||
}, 50);
|
||||
const clear = setTimeout(() => setHighlightId(null), 2000);
|
||||
return () => { clearTimeout(timer); clearTimeout(clear); };
|
||||
}, [highlightId]);
|
||||
|
||||
async function handleStatus(id: number, status: BookingStatus) {
|
||||
if (status === "confirmed" && onConfirm) {
|
||||
onConfirm(id);
|
||||
@@ -41,7 +53,13 @@ export function GenericBookingsList<T extends BaseBooking>({
|
||||
}
|
||||
const prev = items.find((b) => b.id === id);
|
||||
const prevStatus = prev?.status;
|
||||
onItemsChange((list) => list.map((b) => b.id === id ? { ...b, status } : b));
|
||||
// Move changed item to front so it appears first in its status group after sort
|
||||
onItemsChange((list) => {
|
||||
const item = list.find((b) => b.id === id);
|
||||
if (!item) return list;
|
||||
return [{ ...item, status }, ...list.filter((b) => b.id !== id)];
|
||||
});
|
||||
setHighlightId(id);
|
||||
try {
|
||||
const res = await adminFetch(endpoint, {
|
||||
method: "PUT",
|
||||
@@ -85,8 +103,10 @@ export function GenericBookingsList<T extends BaseBooking>({
|
||||
}
|
||||
|
||||
function renderItem(item: T, isArchived: boolean) {
|
||||
const isHighlighted = highlightId === item.id;
|
||||
return (
|
||||
<BookingCard key={item.id} status={item.status}>
|
||||
<div key={item.id} ref={isHighlighted ? highlightRef : undefined}>
|
||||
<BookingCard status={item.status} highlight={isHighlighted}>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-2 flex-wrap text-sm min-w-0">
|
||||
<span className="font-medium text-white truncate max-w-[200px]">{item.name}</span>
|
||||
@@ -104,6 +124,7 @@ export function GenericBookingsList<T extends BaseBooking>({
|
||||
</div>
|
||||
<InlineNotes value={item.notes || ""} onSave={(notes) => handleNotes(item.id, notes)} />
|
||||
</BookingCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user