feat: MC admin — collapsible cards, filters, photo preview, validation, archive

- Collapsible cards with title + hall in label, archive badge
- Archived MCs sorted to bottom, dimmed with "Архив" badge
- Cards have hover + focus-within gold border highlight
- Date validation: error text for missing dates and invalid time ranges
- Search by title/trainer + filter by date (upcoming/past) and hall
- Photo preview with hover overlay (like trainer page)
- ArrayEditor: hiddenItems, getItemBadge props, focus-within styles
This commit is contained in:
2026-03-26 00:43:09 +03:00
parent 6c485872b0
commit 64e923460f
2 changed files with 386 additions and 180 deletions

View File

@@ -13,6 +13,8 @@ interface ArrayEditorProps<T> {
addLabel?: string;
collapsible?: boolean;
getItemTitle?: (item: T, index: number) => string;
getItemBadge?: (item: T, index: number) => React.ReactNode;
hiddenItems?: Set<number>;
}
export function ArrayEditor<T>({
@@ -24,6 +26,8 @@ export function ArrayEditor<T>({
addLabel = "Добавить",
collapsible = false,
getItemTitle,
getItemBadge,
hiddenItems,
}: ArrayEditorProps<T>) {
const [dragIndex, setDragIndex] = useState<number | null>(null);
const [insertAt, setInsertAt] = useState<number | null>(null);
@@ -146,14 +150,15 @@ export function ArrayEditor<T>({
if (dragIndex === null || insertAt === null) {
return items.map((item, i) => {
const isCollapsed = collapsible && collapsed.has(i) && newItemIndex !== i;
const isHidden = hiddenItems?.has(i) ?? false;
const title = getItemTitle?.(item, i) || `#${i + 1}`;
return (
<div
key={i}
ref={(el) => { itemRefs.current[i] = el; }}
className={`rounded-lg border bg-neutral-900/50 mb-3 hover:border-white/25 hover:bg-neutral-800/50 transition-all ${
className={`rounded-lg border bg-neutral-900/50 mb-3 hover:border-white/25 hover:bg-neutral-800/50 focus-within:border-gold/50 focus-within:bg-neutral-800 transition-all ${
newItemIndex === i ? "border-gold/40 ring-1 ring-gold/20" : "border-white/10"
}`}
} ${isHidden ? "hidden" : ""}`}
>
<div className={`flex items-center justify-between gap-2 p-4 ${isCollapsed ? "" : "pb-0 mb-3"}`}>
<div className="flex items-center gap-2 flex-1 min-w-0">
@@ -170,6 +175,7 @@ export function ArrayEditor<T>({
className="flex items-center gap-2 flex-1 min-w-0 text-left cursor-pointer group"
>
<span className="text-sm font-medium text-neutral-300 truncate group-hover:text-white transition-colors">{title}</span>
{getItemBadge?.(item, i)}
<ChevronDown size={14} className={`text-neutral-500 transition-transform duration-200 shrink-0 ${isCollapsed ? "" : "rotate-180"}`} />
</button>
)}
@@ -231,7 +237,7 @@ export function ArrayEditor<T>({
<div
key={i}
ref={(el) => { itemRefs.current[i] = el; }}
className="rounded-lg border border-white/10 bg-neutral-900/50 p-4 mb-3 hover:border-white/25 hover:bg-neutral-800/50 transition-colors"
className="rounded-lg border border-white/10 bg-neutral-900/50 p-4 mb-3 hover:border-white/25 hover:bg-neutral-800/50 focus-within:border-gold/50 focus-within:bg-neutral-800 transition-colors"
>
<div className="flex items-start justify-between gap-2 mb-3">
<div